Codeql系列1—環(huán)境安裝與基礎應用

codeql官網(wǎng):https://codeql.github.com/

1. 環(huán)境安裝

根據(jù)官網(wǎng)的提示,先在Visual Studio Code中安裝Codeql擴展??梢詤⒖迹?a target="_blank">https://marketplace.visualstudio.com/items?itemName=github.vscode-codeql

這個Codeql擴展使用Codeql CLI來編譯和運行查詢。從github上下載Codeql CLI。
https://github.com/github/codeql-cli-binaries/releases

Mac系統(tǒng)對應codeql-osx64.zip的版本,下載后解壓到Documents文件夾(Downloads文件夾不被允許),編輯配置文件~/.bash_profile加入如下配置

export PATH=/Users/axisx/Documents/codeql:$PATH

使用source命令生效后,在命令行中輸入codeql,出現(xiàn)如下顯示即安裝成功

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日已經(jīng)下線,將LGTM底層的CodeQL分析技術原生集成到GitHub,現(xiàn)在只能用上述方式來運行。

2. codeql 創(chuàng)建數(shù)據(jù)庫

codeql cli能成功運行后,就可以通過相關命令來查詢。相關命令手冊參考官網(wǎng):https://docs.github.com/zh/code-security/codeql-cli/codeql-cli-manual

通過codeql cli來做掃描,主要是兩步,database create創(chuàng)建數(shù)據(jù)庫來存儲程序的層次結構,database analyze運行查詢來分析每個CodeQL數(shù)據(jù)庫,并將結果匯總到SARIF文件中。

(1)創(chuàng)建數(shù)據(jù)庫
查看創(chuàng)建數(shù)據(jù)庫的官方文檔: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在執(zhí)行codeql命令創(chuàng)建數(shù)據(jù)庫時,出現(xiàn)了一個報錯。Library文件夾下的操作都是operation not permitted。解決方法是進入系統(tǒng)偏好設置>安全和隱私->完全磁盤訪問,勾選“終端”。給終端訪問磁盤的權限。

一般需要指定--source-root,即要掃描的源碼文件夾路徑。否則會把系統(tǒng)上的文件都掃描一遍。另外,如果是為編譯型語言(C/C++, C#, Go, Java, Swift)創(chuàng)建數(shù)據(jù)庫,需要用--command參數(shù)加入編譯命令。以java為例,命令如下。

database create <數(shù)據(jù)庫路徑> --language="java"  --command="mvn clean install --file pom.xml" --source-root=<要掃描的項目路徑>

(2)查詢分析數(shù)據(jù)庫
基本語句如下。format是指結果文件的格式,包含CSV、SARIF和Graph格式。

codeql database analyze <database> --format=<format> --output=<output>

codeql database analyze命令主要用于自動化執(zhí)行預定義的安全分析,并生成可用于報告和審查的結果。但也可以手動查詢,查詢語句需要符合QL語言。https://codeql.github.com/docs/ql-language-reference/

使用analyze生成csv,會發(fā)現(xiàn)報告中漏洞位置后面有四個數(shù)字,如"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)源碼導入
在創(chuàng)建數(shù)據(jù)庫時要指定掃描源碼的路徑。如果是github上的項目的話,codeql提供了直接導入url來生成database的功能。點擊DATABASES旁邊的github圖標。然后將repository

github項目導入databases

3. codeql中的基本元素

既然是要寫查詢語句,就要像了解語句中基本的元素。這些元素都是為了能夠?qū)幊陶Z言更好的解析做的設計。以Java為例介紹一下,其他元素含義參考官網(wǎng):https://codeql.github.com/codeql-standard-libraries/java/index.html

(1) 表達式Expr。表達式簡單理解就是程序中能產(chǎn)生一個值的代碼。它有很多具體劃分,例如邏輯表達式、i++、switch case等等。具體的Expr元素查看:https://codeql.github.com/codeql-standard-libraries/java/semmle/code/java/Expr.qll/module.Expr.html

(2) 變量Variable。
變量內(nèi)容較少,主要有四種。

LocalScopeVariable 局部變量。LocalVariableDecl像是LocalScopeVariable的一種特例,特指在代碼中顯式聲明的局部變量,如函數(shù)中定義的int a=1。
LocalVariableDecl 局部變量聲明。通常用于表示在函數(shù)、方法或代碼塊內(nèi)部聲明的局部變量。
Parameter 形式參數(shù)。通常用于表示函數(shù)或方法定義中的參數(shù)
Variable是通用的概念,可以表示程序中的任何變量。

(3) 類型Type
類型包含基本類型PrimitiveType、數(shù)組類型Array、引用類型RefType(包含類Class、接口Interface)等。引用類型可以位于頂層 ( TopLevelType) 或嵌套 (NestedType)。類和接口也可以是本地的 ( LocalClassOrInterface, LocalClass) 或匿名的 ( AnonymousClass)。枚舉類型 (EnumType) 和記錄 (Record) 是特殊類型的類。

(4) 類Member

Callable: 代表可調(diào)用實體,通常包括函數(shù)、方法、函數(shù)指針等。a()是構造函數(shù)。A.a()就是可調(diào)用實體
Constructor:構造函數(shù)
Member:類成員的通用抽象,包含方法、構造函數(shù)、字段等
Field: 類或?qū)嵗侄?StaticInitializer: static字段或方法

(5) 聲明Statement
Statement代表程序中的語句。語句通常用于執(zhí)行特定的操作、控制程序的執(zhí)行流程或引入控制結構,例如賦值語句、條件語句、循環(huán)語句等。這些語句中就可能包含Expr表達式來計算值。

Stmt: 所有類型Statement的父類
BlockStmt: 
CatchClause: try...catch
ConstCase: switch
ConditionalStmt: if, for, while, dowhile
ForStmt: 循環(huán)
JumpStmt:break, yield, continue
...

Callable庫是方法調(diào)用相關的,Generics庫是泛型相關的。
另外,codeql針對JDK、Struts2、Spring、Android。分別開發(fā)相應的library,更好的解析其中的內(nèi)容。

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. 數(shù)據(jù)流分析

數(shù)據(jù)流分析用于計算變量在程序中各個點保存的可能值,確定這些值如何在程序中傳播以及使用它們的位置。

本地的數(shù)據(jù)流分析的元素位于DataFlow模塊。數(shù)據(jù)流可以經(jīng)過的類節(jié)點定義為Node。Node又分為ExprNodeParameterNode。在數(shù)據(jù)流的基礎上,如果定義某個變量是污點,那么如果從Node From到Node To存在邊,污點跟蹤TaintTracking就成立。

同樣看一下官網(wǎng)的一些案例。官網(wǎng)的數(shù)據(jù)流分析主要分為Local data flow局部數(shù)據(jù)流和Global data flow全局數(shù)據(jù)流。局部數(shù)據(jù)流分析一般指函數(shù)、方法或代碼塊內(nèi)部流動。全局數(shù)據(jù)流則覆蓋了整個代碼庫,可以跟蹤所有變量、函數(shù)調(diào)用之間的數(shù)據(jù)流關系。

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類,可以對方法構造函數(shù)等進行調(diào)用。其getCallee()方法是獲取可調(diào)用的目標。

b. 查找對格式字符串未硬編碼的格式化函數(shù)的調(diào)用。
格式化代碼一般為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."

現(xiàn)在的版本MethodAccessMethodCall,也就是找到調(diào)用方法為String.format()方法。format.getFormatStringIndex()是一個用于獲取格式化字符串參數(shù)的方法,返回的是格式化字符串在參數(shù)列表中的索引值。然后獲取這個索引值的參數(shù)。not exists表示不存在數(shù)據(jù)流路徑,也就是String.format中的值不是傳入的,這樣就不存在數(shù)據(jù)流。asExpr()將節(jié)點source和sink都轉(zhuǎn)換成表達式節(jié)點。判斷source為字符串常量,sink為格式化字符串函數(shù)。

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。數(shù)據(jù)流的源Node如果是字符串(非變量),傳入到URL的第一個參數(shù)中,那么URL就是硬編碼的。

Global data flow

局部數(shù)據(jù)流是DataFlow::localFlow,全局數(shù)據(jù)流是DataFlow::Global<ConfigSig>。全局數(shù)據(jù)流包含四個重點謂詞

isSource :定義數(shù)據(jù)從哪兒流出
isSink:定義數(shù)據(jù)流向哪兒
isBarrier: 限制數(shù)據(jù)流(可選項)
isAdditionalFlowStep: 添加額外的流程步驟 (可選項)

全局數(shù)據(jù)流分析的基本格式如下

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()

全局污點跟蹤針對全局數(shù)據(jù)流,所以基本格式與上述全局數(shù)據(jù)流分析格式相似。只需要把DataFlow換成TaintTracking。

官方給的一些Global data flow的案例:

a. 使用全局數(shù)據(jù)流編寫一個查詢,查找所有用硬編碼字符串創(chuàng)建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"

和局部數(shù)據(jù)流分析很類似,只需要用全局數(shù)據(jù)流的格式寫即可。只不過局部變量用from先把用到的變量類型聲明了一遍,但是在全局數(shù)據(jù)流分析中在exists函數(shù)中聲明的變量類型。

b. 編寫一個類來表示從java.lang.System.getenv傳遞的數(shù)據(jù)流。該方法的示例代碼如: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在現(xiàn)在的版本里已經(jīng)改為MethodCall。有關方法的操作都位于Method中,而在數(shù)據(jù)流中對應的是MethodCall。首先獲取數(shù)據(jù)流MethodCall中對應的方法,判斷這個方法名是否為getenv,判斷聲明這個方法的類型是否為java.lang.System。由于Codeql中集成了JDK的庫。在JDK.qll中有如下的代碼。所以只需要判斷類型是否為TypeSystem

class TypeSystem extends Class {
  TypeSystem() { this.hasQualifiedName("java.lang", "System") }
}

結合a和b的案例,就可以寫一個全局數(shù)據(jù)流分析,從getenvjava.net.url

c. 編寫一個查詢來查找未被任何其他方法調(diào)用的方法

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 # 這兩個是隱式調(diào)用的可以排除
    not callee.isPublic() and 
    not callee.(Constructor).getNumberOfParameters() = 0 and
    not callee.getDeclaringType() instanceof TestClass
select callee, "Not called."

代碼庫中的方法很多都不會被調(diào)用,所以在查詢時應該將庫中的方法排除。也就是檢查是否是源文件。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容