標題:CodeQL 基礎

taibeihacker

Moderator

CodeQL 基础​

1 前言​

1.1 背景​

CodeQL 是一個代碼分析平台,在它的幫助下,安全研究人員可以利用已知的安全漏洞來挖掘類似的漏洞。
CodeQL 就是一種代碼分析平台。借助這個平台,安全研究人員可以實現變種分析的自動化。這裡所謂的變種分析,就是以已知的安全漏洞作為參照物,在我們的目標代碼中尋找類似的安全問題的過程,也就是照葫蘆畫瓢的過程。
此外,為了提高安全分析人員的工作效率,CodeQL 平台還提供了許多有用的工具、腳本、查詢和代碼庫。

1.2 相关概念​

1.2.1 CodeQL 核心​

QL 语言在靜態程序分析學科中,通常採用一種Data-Log 的聲明式語言來代替命令式語言進行結果分析,具體可以參考「靜態程序分析」文章。而QL 語言便是Data-Log 語言的一種。
QL 数据库CodeQL 數據庫中存放的是使用CodeQL 創建和分析的關係數據。 可以將其看作是目標代碼的中間分析產物。

1.2.2 CodeQL 工作原理​

CodeQL 工作流程:
將代碼創建成數據庫
編寫QL查詢從數據庫中查詢代碼
解釋查詢結果

1.2.2.1 数据库创建​

使用語言相關的extractor 從代碼中提取抽象語法樹(ast)、名稱綁定的語義和類型信息,把源代碼轉化成單關係表示(single relational representation),以CodeQL 數據庫存儲。而在CodeQL 中,是通過一種CSV flow 模型來作為中間代碼的。
此外,每種語言都有自己獨特的數據庫模式,用於定義創建數據庫的關係。該圖為提取過程中的初始詞彙分析與使用CodeQL 的實際複雜分析提供了界面。

1.2.2.2 执行查询​

使用CodeQL 專門設計的面向對象語言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中
echo 'export PATH=\$PATH:/path/to/codeql' ~/.zshrc
source ~/.zshrc
然後需要下載相關庫文件:https://github.com/Semmle/ql。庫文件是開源的,後續要做的是根據這些庫文件來編寫QL 腳本。
之後,需要在VSCode 上安裝對應的擴展,在應用商店中搜索CodeQL 即可。安裝之後,需要在擴展設置裡配置CLI 文件的位置。
202202141536600.png-water_print

此外,還有一種快捷配置的方式,即:start workspace 項目。
注意:該工作區內含了QL 庫,因此一定要使用遞歸方式來下拉工作區代碼。遞歸方式下拉該倉庫後,不需要再下載https://github.com/Semmle/ql 這個庫了。
1
git clone --recursive [email protected]:github/vscode-codeql-starter.git
在配置好環境之後,就可以利用CLI 工具來創建數據庫了。以Java 代碼為例,使用如下命令創建:
1
codeql database create database-folder --language=java --command='mvn clean install --file pom.xml'
技巧
如果省略--command參數,則codeQL 會自動檢測並使用自己的工具來構建。但還是強烈推薦使用自己自定義的參數,尤其是大項目時。
建立好的數據庫,其目錄結構為:
1
2
3
4
- log/# 輸出的日誌信息
- db-java/# 編譯的數據庫
- src.zip # 編譯所對應的目標源碼
- codeql-database.yml # 數據庫相關配置
除了在本地構建數據庫外,CodeQL 還提供了在線版本:LGTM.com。一方面,可以在其上面直接搜索開源項目,下載數據庫;另一方面,也可以上傳代碼,後台會自動生成代碼數據庫。同時,在選定項目後,也可以在線查詢,十分方便。
最後在VSCode 中,點擊「打開工作區」來打開剛剛下拉的vscode-codeql-starter 工作區,在CodeQL 插件裡,打開剛剛生成的database。
然後編寫自己的CodeQL 腳本,並將腳本保存至vscode-codeql-starter/codeql-custom-queries-java 處,這樣import 模塊時就可以正常引用。將編寫的ql 腳本在VSCode 中打開,之後點擊CodeQL 插件中的Run Query,即可開始查詢。

2 QL 语法​

2.1 谓词​

在CodeQL 中,函數並不叫“函數”,叫做Predicates(謂詞)。為了便於說明,下文中的函數與謂詞都是指代同一個內容。
謂詞的定義方式如下:
1
2
3
4
predicate name(type arg)
{
statements
}
定義謂詞有三個要素:
關鍵詞predicate(如果沒有返回值),或者結果的類型(如果當前謂詞內存在返回值)
謂詞的名稱
謂詞的參數列表
謂詞主體

2.1.1 无返回值的谓词​

無返回值的謂詞以predicate關鍵詞開頭。若傳入的值滿足謂詞主體中的邏輯,則該謂詞將保留該值。
無返回值謂詞的使用範圍較小,但仍然在某些情況下扮演了很重要的一個角色
舉一個簡單的例子
1
2
3
4
5
6
7
8
predicate isSmall(int i) {
i in [1 . 9]
}
from int i
where isSmall(i) //將整數集合i從正無窮大的數據集含,限制至1-9
select i
//輸出1-9 的數字
若傳入的i 是小於10 的正整數,則isSmall(i) 將會使得傳入的集合i 只保留符合條件的值,其他值將會被捨棄。

2.1.2 有返回值的谓词​

當需要將某些結果從謂詞中返回時,與編程語言的return 語句不同的是,謂詞使用的是一個特殊變量result。謂詞主體的語法只是為了表述邏輯之間的關係,因此務必不要用一般編程語言的語法來理解。
1
2
3
4
5
6
7
int getSuccessor(int i) {
//若傳入的i 位於1-9 內,則返回i+1
result=i + 1 and i in [1 . 9]
}
select getSuccessor(3) //輸出4
select getSuccessor(33) //不輸出任何信息
在謂詞主體中,result 變量可以像一般變量一樣正常使用,唯一不同的是這個變量內的數據將會被返回。
1
2
3
4
5
6
7
8
9
10
11
12
string getANeighbor(string country) {
country='France' and result='Belgium'
or
country='France' and result='Germany'
or
country='Germany' and result='Austria'
or
country='Germany' and result='Belgium'
}
select getANeighbor('France')
//返回兩個條目,'Belgium' 與'Germany'
謂詞不允許描述的數據集合個數不限于有限数量大小的。舉個例子:
1
2
3
4
5
6
//該謂詞將使得編譯報錯
int multiplyBy4(int i) {
//i 是一個數據集合,此時該集合可能是「無限大小」
//result 集合被設置為i*4,意味著result 集合的大小有可能也是無限大小
result=i * 4
}
但如果我們仍然需要定義這類函數,則必須限制集合数据大小,同時添加一個bindingset 標註。該標註將會聲明謂詞plusOne 所包含的數據集合是有限的,前提是i 綁定到有限數量的數據集合。
1
2
3
4
5
6
7
8
bindingset[x] bindingset[y]
predicate plusOne(int x, int y) {
x + 1=y
}
from int x, int y
where y=42 and plusOne(x, y)
select x, y

2.2 类​

在CodeQL 中的類,并不意味着建立一个新的对象,而只是表示特定一類的數據集合,定義一個類,需要三個步驟:
使用關鍵字class
起一個類名,其中類名必須是首字母大寫的。
確定是從哪個類中派生出來的
其中,基本類型boolean、float、int、string 以及date 也算在內。
如下是官方的一個樣例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class OneTwoThree extends int {
OneTwoThree() { //characteristic predicate
this=1 or this=2 or this=3
}
string getAString() { //member predicate
result='One, two or three: ' + this.toString()
}
predicate isEven() { //member predicate
this in [1 . 2]
}
}
from OneTwoThree i
where i=1 or i.getAString()='One, two or three: 2'
select i
//輸出1 和2
其中,特征谓词類似於類的構造函數,它將會進一步限制當前類所表示數據的集合。它將數據集合從原先的Int 集,進一步限制至1-3 這個範圍。 this 變量表示的是當前類中所包含的數據集合。與result 變量類似,this 同樣是用於表示數據集合直接的關係。
此外,在特徵謂詞中,比較常用的一個關鍵字是exists。該關鍵字的語法如下:
1
2
3
4
exists(variable declarations | formula)
//以下兩個exists 所表達的意思等價。
exists(variable declarations | formula 1 | formula 2
exists(variable declarations | formula 1 and formula 2
這個關鍵字的使用引入了一些新的變量。如果變量中至少有一組值可以使formula 成立,那麼該值將被保留。
一個簡單的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import cpp
class NetworkByteSwap extends Expr{
NetworkByteSwap()
{
//對於MacroInvocation這個大類的數據集合來說,
exists(MacroInvocation mi |
//如果存在宏調用,其宏名稱滿足特定正則表達式
mi.getMacroName().regexpMatch('ntoh(s|l|ll)') and
//將這類數據保存至當前類中
this=mi.getExpr()
)
}
}
from NetworkByteSwap n
select n, 'Network byte swap'

3 CodeQL U-Boot Challenge​

在Github Learning Lab 中,有一個用於學習CodeQL 的入門課程- CodeQL U-Boot Challenge (C/C++)](https://lab.github.com/GitHubtraining/codeql-u-boot-challenge-(cc++))
編寫一個簡單的查詢,用於查詢strlen函數的定義位置。
1
2
3
4
5
import cpp
from Function f
where f.getName()='strlen'
select f, 'a function named strlen'
分析這個簡單的查詢,之後查詢一下memcpy函數
1
2
3
4
5
import cpp
from Function f
where f.getName()='memcpy'
select f, 'a function named memcpy'
使用不同的類以及不同的謂詞。這裡我們編寫QL 查找名為ntohs、ntohl 以及ntohll的宏定義。
1
2
3
4
5
6
import cpp
from Macro macro
# where macro.getName()='ntohs' or macro.getName()='ntohl' or macro.getName()='ntohll'
where macro.getName().regexpMatch('ntoh(s|l|ll)')
select macro
使用雙變量。通過使用多個變量來描述複雜的代碼關係,查詢特定函數的調用位置。
1
2
3
4
5
import cpp
from FunctionCall c, Function f
where c.getTarget()=f and f.getName()=='memcpy'
select c
使用Step6 的技巧,查詢宏定義的調用位置。
1
2
3
4
5
import cpp
from MacroInvocation mi
where mi.getMacro().getName().regexpMatch('ntoh(s|l|ll)')
select mi
改變select 的輸出。查找這些宏調用所擴展到的頂級表達式(宏展開)。
1
2
3
4
5
import cpp
from MacroInvocation mi
where mi.getMacro().getName().regexpMatch('ntoh(s|l|ll)')
select mi.getExpr() # 注意這裡的.getExpr()
實現一個類。用exists 關鍵字來引入一個臨時變量,以設置當前類的數據集合;特徵謂詞在聲明時會被調用以確定當前類的範圍,類似於C++ 構造函數。
特徵謂詞在聲明時會被調用以確定當前類的範圍,類似於C++ 構造函數。查詢語句中的類中,先通過exists 量詞創建一個臨時變量mi 來表示被調用的宏的名字,如果被調用的的宏展開後和當前代碼片段相等,則這個表達式屬於這個集合。
1
2
3
4
5
6
7
8
9
10
11
12
13
import cpp
class NetworkByteSwap extends Expr {
NetworkByteSwap() {
exists(MacroInvocation mi |
mi.getMacroName().regexpMatch('ntoh(s|l|ll)') and
this=mi.getExpr()
)
}
}
from NetworkByteSwap n
select n, 'Network byte swap'
污點追踪
借助前面幾步,基本描述了CodeQL 的使用。最後一個測試是使用CodeQL 進行污點追踪。這裡使用了CodeQL 的全局污點追踪(Global taint tracking)。新定義的Config 類繼承於TaintTracking:Configuration。類中重載的isSource 謂語定義為污點的源頭,而isSink 定義為污點匯聚點。
有時候,遠程輸入的數據可能經過ntoh 函數處理,通過轉換字節序得到相應的數字。而memcpy 的第2 個參數如果控制不當,可造成數據溢出。將上面兩個結論結合起來,如果有一個遠程輸入的數據通過字節序變換得到的數字,在未經過校驗的情況下,作為了memcpy 的第二個參數,那麼就有可能造成數據溢出。
在isSource 中,判斷source 的Expr 是否是NetworkByteSwap 這個類,來判斷污點的源頭。
在isSink 中,我們使用了輔助類FunctionCall 判斷函數調用是否為memcpy 且sink 的代碼片段是否為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
import cpp
import semmle.code.cpp.dataflow.TaintTracking
import DataFlow:PathGraph
# 設置用於交換網絡數據的類
class NetworkByteSwap extends Expr {
NetworkByteSwap() {
exists(MacroInvocation mi |
mi.getMacroName().regexpMatch('ntoh(s|l|ll)') and
this=mi.getExpr()
)
}
}
# 設置污點跟踪的分析信息
class Config extends TaintTracking:Configuration {
Config() { this='NetworkToMemFuncLength' }
override predicate isSource(DataFlow:Node source) { source.asExpr() instanceof NetworkByteSwap }
override predicate isSink(DataFlow:Node sink) {
exists(FunctionCall call |
call.getTarget().getName()='memcpy' and
sink.asExpr()=call.getArgument(2) and
not call.getArgument(1).isConstant()
)
}
}
# 查詢
from Config cfg, DataFlow:PathNode source, DataFlow:PathNode sink
where cfg.hasFlowPath(source, sink)
select sink, source, sink, 'Network byte swap flows to memcpy'

4 CodeQL for Java​

4.1 基本查询​

對if 語句中的冗餘代碼進行搜索,例如空的then 分支,示例代碼如下:
1
if (error) {}
編寫查詢語句如下:
1
2
3
4
5
6
7
8
9
10
11
# 引入Java 標準查詢庫
import java
# 定義查詢變量,聲明IfStmt 變量代表if 語句
# 聲明BlockStmt 變量代表then 代碼塊
from IfStmt ifstmt, BlockStmt block
# 定義查詢的限制條件
where ifstmt.getThen()=block and
block.getNumStmt()=0
# 將結果返回到控制台select program element, 'alert message'
select ifstmt, 'This 'if' statement is redundant.'

查询优化​

編寫QL 代碼的過程是一個迭代的過程,在最初的查詢結果中可能會出現一些「非預期」的結果,因此需要通過不斷修改,來完善QL 查詢代碼。
在如下示例代碼中,空的else if 分支的確有著自己的用途,因此優化查詢:當if 語句中具有else 分支時,認為空分支有自己的作用,忽略空分支。
1
2
3
4
5
6
7
if (.) {
.
} else if ('-verbose'.equals(option)) {
//nothing to do - handled earlier
} else {
error('unrecognized option');
}
查詢語句優化:
1
2
3
where ifstmt.
 
返回
上方