codeql官网:https://codeql.github.com/
1. 环境安装
根据官网的提示,先在Visual Studio Code中安装Codeql扩展。可以参考:https://marketplace.visualstudio.com/items?itemName=github.vscode-codeql
这个Codeql扩展使用Codeql CLI来编译和运行查询。从github上下载Codeql CLI。
https://github.com/github/codeql-cli-binaries/releases
Mac系统对应codeql-osx64.zip
的版本,下载后解压到Documents文件夹(Downloads文件夹不被允许),编辑配置文件~/.bash_profile
加入如下配置
export PATH=/Users/axisx/Documents/codeql:$PATH
使用source命令生效后,在命令行中输入codeql,出现如下显示即安装成功
axisx@loaclhost Documents % codeql
Usage: codeql <command> <argument>...
Create and query CodeQL databases, or work with the QL language.
PS:之前很多人推荐的在线运行平台:https://lgtm.com/query。该平台在2022年12月16日已经下线,将LGTM底层的CodeQL分析技术原生集成到GitHub,现在只能用上述方式来运行。
2. codeql 创建数据库
codeql cli能成功运行后,就可以通过相关命令来查询。相关命令手册参考官网:https://docs.github.com/zh/code-security/codeql-cli/codeql-cli-manual
通过codeql cli来做扫描,主要是两步,database create
创建数据库来存储程序的层次结构,database analyze
运行查询来分析每个CodeQL数据库,并将结果汇总到SARIF文件中。
(1)创建数据库
查看创建数据库的官方文档:https://docs.github.com/en/code-security/codeql-cli/getting-started-with-the-codeql-cli/preparing-your-code-for-codeql-analysis
基础语句为如下。支持的语言包含:C/C++, C#, Go, Java, Kotlin, JavaScript/TypeScript, Python, Ruby, Swift。
codeql database create <database> --language=<language-identifier>
Mac在执行codeql命令创建数据库时,出现了一个报错。Library文件夹下的操作都是operation not permitted
。解决方法是进入系统偏好设置>安全和隐私->完全磁盘访问,勾选“终端”。给终端访问磁盘的权限。
一般需要指定--source-root
,即要扫描的源码文件夹路径。否则会把系统上的文件都扫描一遍。另外,如果是为编译型语言(C/C++, C#, Go, Java, Swift)创建数据库,需要用--command
参数加入编译命令。以java为例,命令如下。
database create <数据库路径> --language="java" --command="mvn clean install --file pom.xml" --source-root=<要扫描的项目路径>
(2)查询分析数据库
基本语句如下。format
是指结果文件的格式,包含CSV、SARIF和Graph格式。
codeql database analyze <database> --format=<format> --output=<output>
codeql database analyze
命令主要用于自动化执行预定义的安全分析,并生成可用于报告和审查的结果。但也可以手动查询,查询语句需要符合QL语言。https://codeql.github.com/docs/ql-language-reference/
使用analyze生成csv,会发现报告中漏洞位置后面有四个数字,如"27, 64, 27, 66" ,它们表示代码中涉及潜在问题的起始行、起始列、结束行和结束列。以下面这个脚本为例,27行64列-27行66列,即url.openConnection();
的url
@RequestMapping(value = "/ssrf")
public String One(@RequestParam(value = "url") String imageUrl) {
try {
URL url = new URL(imageUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
...
}
(3)源码导入
在创建数据库时要指定扫描源码的路径。如果是github上的项目的话,codeql提供了直接导入url来生成database的功能。点击DATABASES旁边的github图标。然后将repository
3. codeql中的基本元素
既然是要写查询语句,就要像了解语句中基本的元素。这些元素都是为了能够对编程语言更好的解析做的设计。以Java为例介绍一下,其他元素含义参考官网:https://codeql.github.com/codeql-standard-libraries/java/index.html
(1) 表达式Expr。表达式简单理解就是程序中能产生一个值的代码。它有很多具体划分,例如逻辑表达式、i++
、switch case
等等。具体的Expr元素查看:https://codeql.github.com/codeql-standard-libraries/java/semmle/code/java/Expr.qll/module.Expr.html
(2) 变量Variable。
变量内容较少,主要有四种。
LocalScopeVariable 局部变量。LocalVariableDecl像是LocalScopeVariable的一种特例,特指在代码中显式声明的局部变量,如函数中定义的int a=1。
LocalVariableDecl 局部变量声明。通常用于表示在函数、方法或代码块内部声明的局部变量。
Parameter 形式参数。通常用于表示函数或方法定义中的参数
Variable是通用的概念,可以表示程序中的任何变量。
(3) 类型Type
类型包含基本类型PrimitiveType
、数组类型Array
、引用类型RefType
(包含类Class
、接口Interface
)等。引用类型可以位于顶层 ( TopLevelType
) 或嵌套 (NestedType
)。类和接口也可以是本地的 ( LocalClassOrInterface, LocalClass
) 或匿名的 ( AnonymousClass
)。枚举类型 (EnumType
) 和记录 (Record)
是特殊类型的类。
(4) 类Member
Callable: 代表可调用实体,通常包括函数、方法、函数指针等。a()是构造函数。A.a()就是可调用实体
Constructor:构造函数
Member:类成员的通用抽象,包含方法、构造函数、字段等
Field: 类或实例字段
StaticInitializer: static字段或方法
(5) 声明Statement
Statement代表程序中的语句。语句通常用于执行特定的操作、控制程序的执行流程或引入控制结构,例如赋值语句、条件语句、循环语句等。这些语句中就可能包含Expr表达式来计算值。
Stmt: 所有类型Statement的父类
BlockStmt:
CatchClause: try...catch
ConstCase: switch
ConditionalStmt: if, for, while, dowhile
ForStmt: 循环
JumpStmt:break, yield, continue
...
Callable库是方法调用相关的,Generics库是泛型相关的。
另外,codeql针对JDK、Struts2、Spring、Android。分别开发相应的library,更好的解析其中的内容。
4. 常用查询
有了对元素的了解,结合ql语法就可以开始写查询语句。以Java为例,介绍一些简单常用的。参考官方文档:https://codeql.github.com/docs/codeql-language-guides/codeql-library-for-java/
a. 查询某种类型的变量,如int类型,示例如下。
import java
from Variable v, PrimitiveType pt
where pt=v.getType() and pt.hasName("int")
select v
b. 查询泛型接口。如public interface Map<K, V>
import java
from GenericInterface map, ParameterizedType pt
where map.hasQualifiedName("java.util", "Map") and
pt.getSourceDeclaration() = map
select pt
c. Expr相关查询
# 查找return为语句的。如果是if语句则是IfStmt
import java
from Expr e
where e.getParent() instanceof ReturnStmt
select e
# 查找方法体
import java
from Stmt s
where s.getParent() instanceof Method
select s
5. 数据流分析
数据流分析用于计算变量在程序中各个点保存的可能值,确定这些值如何在程序中传播以及使用它们的位置。
本地的数据流分析的元素位于DataFlow模块。数据流可以经过的类节点定义为Node
。Node
又分为ExprNode
和ParameterNode
。在数据流的基础上,如果定义某个变量是污点,那么如果从Node From到Node To存在边,污点跟踪TaintTracking
就成立。
同样看一下官网的一些案例。官网的数据流分析主要分为Local data flow
局部数据流和Global data flow
全局数据流。局部数据流分析一般指函数、方法或代码块内部流动。全局数据流则覆盖了整个代码库,可以跟踪所有变量、函数调用之间的数据流关系。
Local data flow
a. 查找传入new FileReader(..)
中的文件名
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)))
# 如果要使源更加具体可以将DataFlow::exprNode(src)换为DataFlow::parameterNode(p)
select src
Call属于Expr类,可以对方法构造函数等进行调用。其getCallee()
方法是获取可调用的目标。
b. 查找对格式字符串未硬编码的格式化函数的调用。
格式化代码一般为String.format("I am %d years old.", age);
import java
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.StringFormat
from StringFormatMethod format, MethodAccess call, Expr formatString
where
call.getMethod() = format and
call.getArgument(format.getFormatStringIndex()) = formatString and
not exists(DataFlow::Node source, DataFlow::Node sink |
DataFlow::localFlow(source, sink) and
source.asExpr() instanceof StringLiteral and
sink.asExpr() = formatString
)
select call, "Argument to String format method isn't hard-coded."
现在的版本MethodAccess
是MethodCall
,也就是找到调用方法为String.format()
方法。format.getFormatStringIndex()
是一个用于获取格式化字符串参数的方法,返回的是格式化字符串在参数列表中的索引值。然后获取这个索引值的参数。not exists
表示不存在数据流路径,也就是String.format
中的值不是传入的,这样就不存在数据流。asExpr()
将节点source和sink都转换成表达式节点。判断source为字符串常量,sink为格式化字符串函数。
c.查找所有硬编码字符串java.net.URL
import semmle.code.java.dataflow.DataFlow
from Constructor url, Call call, StringLiteral src
where
url.getDeclaringType().hasQualifiedName("java.net", "URL") and
call.getCallee() = url and
DataFlow::localFlow(DataFlow::exprNode(src), DataFlow::exprNode(call.getArgument(0)))
select src
这个有了上面的分析理解起来就很简单了。重点在于StringLiteral
,它代表字符串或text block。数据流的源Node如果是字符串(非变量),传入到URL的第一个参数中,那么URL就是硬编码的。
Global data flow
局部数据流是DataFlow::localFlow
,全局数据流是DataFlow::Global<ConfigSig>
。全局数据流包含四个重点谓词
isSource :定义数据从哪儿流出
isSink:定义数据流向哪儿
isBarrier: 限制数据流(可选项)
isAdditionalFlowStep: 添加额外的流程步骤 (可选项)
全局数据流分析的基本格式如下
import semmle.code.java.dataflow.DataFlow
module MyFlowConfiguration implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) { ... }
predicate isSink(DataFlow::Node sink) { ... }
}
module MyFlow = DataFlow::Global<MyFlowConfiguration>;
from DataFlow::Node source, DataFlow::Node sink
where MyFlow::flow(source, sink)
select source, "Data flow to $@.", sink, sink.toString()
全局污点跟踪针对全局数据流,所以基本格式与上述全局数据流分析格式相似。只需要把DataFlow
换成TaintTracking
。
官方给的一些Global data flow的案例:
a. 使用全局数据流编写一个查询,查找所有用硬编码字符串创建java.net.URL
的。
import semmle.code.java.dataflow.DataFlow
module LiteralToURLConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
source.asExpr() instanceof StringLiteral
}
predicate isSink(DataFlow::Node sink) {
exists(Call call |
sink.asExpr() = call.getArgument(0) and
call.getCallee().(Constructor).getDeclaringType().hasQualifiedName("java.net", "URL")
)
}
}
module LiteralToURLFlow = DataFlow::Global<LiteralToURLConfig>;
from DataFlow::Node src, DataFlow::Node sink
where LiteralToURLFlow::flow(src, sink)
select src, "This string constructs a URL $@.", sink, "here"
和局部数据流分析很类似,只需要用全局数据流的格式写即可。只不过局部变量用from
先把用到的变量类型声明了一遍,但是在全局数据流分析中在exists
函数中声明的变量类型。
b. 编写一个类来表示从java.lang.System.getenv
传递的数据流。该方法的示例代码如:String javaHome = System.getenv("JAVA_HOME");
import java
class GetenvSource extends MethodAccess {
GetenvSource() {
exists(Method m | m = this.getMethod() |
m.hasName("getenv") and
m.getDeclaringType() instanceof TypeSystem
)
}
MethodAccess
在现在的版本里已经改为MethodCall
。有关方法的操作都位于Method
中,而在数据流中对应的是MethodCall
。首先获取数据流MethodCall
中对应的方法,判断这个方法名是否为getenv
,判断声明这个方法的类型是否为java.lang.System
。由于Codeql中集成了JDK的库。在JDK.qll中有如下的代码。所以只需要判断类型是否为TypeSystem
。
class TypeSystem extends Class {
TypeSystem() { this.hasQualifiedName("java.lang", "System") }
}
结合a和b的案例,就可以写一个全局数据流分析,从getenv
到java.net.url
c. 编写一个查询来查找未被任何其他方法调用的方法
import java
from Callable callee
where not exists(Callable caller | caller.polyCalls(callee)) and
callee.getCompilationUnit().fromSource() and # 是否为源文件
not callee.hasName("<clinit>") and not callee.hasName("finalize") and # 这两个是隐式调用的可以排除
not callee.isPublic() and
not callee.(Constructor).getNumberOfParameters() = 0 and
not callee.getDeclaringType() instanceof TestClass
select callee, "Not called."
代码库中的方法很多都不会被调用,所以在查询时应该将库中的方法排除。也就是检查是否是源文件。