Codeql系列1—环境安装与基础应用

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

github项目导入databases

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模块。数据流可以经过的类节点定义为NodeNode又分为ExprNodeParameterNode。在数据流的基础上,如果定义某个变量是污点,那么如果从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."

现在的版本MethodAccessMethodCall,也就是找到调用方法为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的案例,就可以写一个全局数据流分析,从getenvjava.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."

代码库中的方法很多都不会被调用,所以在查询时应该将库中的方法排除。也就是检查是否是源文件。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容