作者:w7ay@知道創宇404實驗室
日期:2019年11月18日
英文版本: http://www.bjnorthway.com/1079/
QL是一種查詢語言,支持對C++,C#,Java,JavaScript,Python,go等多種語言進行分析,可用于分析代碼,查找代碼中控制流等信息。
之前筆者有簡單的研究通過JavaScript語義分析來查找XSS,所以對于這款引擎有濃厚的研究興趣 。
安裝
1.下載分析程序:https://github.com/github/codeql-cli-binaries/releases/latest/download/codeql.zip
分析程序支持主流的操作系統,Windows,Mac,Linux
2.下載相關庫文件:https://github.com/Semmle/ql
庫文件是開源的,我們要做的是根據這些庫文件來編寫QL腳本。
3.下載最新版的VScode,安裝CodeQL擴展程序:https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-codeql
-
用vscode的擴展可以方便我們看代碼
-
然后到擴展中心配置相關參數

4.

- cli填寫下載的分析程序路徑就行了,windows可以填寫codeql.cmd
- 其他地方默認就行
建立數據庫
以JavaScript為例,建立分析數據庫,建立數據庫其實就是用分析程序來分析源碼。到要分析源碼的根目錄,執行codeql database create jstest --language=javascript

接下來會在該目錄下生成一個jstest的文件夾,就是數據庫的文件夾了。
接著用vscode打開之前下載的ql庫文件,在ql選擇夾中添加剛才的數據庫文件,并設置為當前數據庫。

接著在QL/javascript/ql/src目錄下新建一個test.ql,用來編寫我們的ql腳本。為什么要在這個目錄下建立文件呢,因為在其他地方測試的時候import javascript導入不進來,在這個目錄下,有個javascript.qll就是基礎類庫,就可以直接引入import javascript,當然可能也有其他的方法。
看它的庫文件,它基本把JavaScript中用到的庫,或者其他語言的定義語法都支持了。

輸出一段hello world試試?

語義分析查找的原理
剛開始接觸ql語法的時候可能會感到它的語法有些奇怪,它為什么要這樣設計?我先說說自己之前研究基于JavaScript語義分析查找dom-xss是怎樣做的。
首先一段類似這樣的javascript代碼
var param = location.hash.split("#")[1];
document.write("Hello " + param + "!");
常規的思路是,我們先找到document.write函數,由這個函數的第一個參數回溯尋找,如果發現它最后是location.hash.split("#")[1];,就尋找成功了。我們可以稱document.write為sink,稱location.hash.split為source。基于語義分析就是由sink找到source的過程(當然反過來找也是可以的)。
而基于這個目標,就需要我們設計一款理解代碼上下文的工具,傳統的正則搜索已經無法完成了。
第一步要將JavaScript的代碼轉換為語法樹,通過pyjsparser可以進行轉換
from pyjsparser import parse
import json
html = '''
var param = location.hash.split("#")[1];
document.write("Hello " + param + "!");
'''
js_ast = parse(html)
print(json.dumps(js_ast)) # 它輸出的是python的dict格式,我們用轉換為json方便查看
最終就得到了如下一個樹結構

這些樹結構的一些定義可以參考:https://esprima.readthedocs.io/en/3.1/syntax-tree-format.html
大概意思可以這樣理解:變量param是一個Identifier類型,它的初始化定義的是一個MemberExpression表達式,該表達式其實也是一個CallExpression表達式,CallExpression表達式的參數是一個Literal類型,而它具體的定義又是一個MemberExpression表達式。
第二步,我們需要設計一個遞歸來找到每個表達式,每一個Identifier,每個Literal類型等等。我們要將之前的document.write轉換為語法樹的形式
{
"type":"MemberExpression",
"object":{
"type":"Identifier",
"name":"document"
},
"property":{
"type":"Identifier",
"name":"write"
}
}
location.hash也是同理
{
"type":"MemberExpression",
"object":{
"type":"Identifier",
"name":"location"
},
"property":{
"type":"Identifier",
"name":"hash"
}
}
在找到了這些sink或source后,再進行正向或反向的回溯分析。回溯分析也會遇到不少問題,如何處理對象的傳遞,參數的傳遞等等很多問題。之前也基于這些設計寫了一個在線基于語義分析的demo
QL語法
QL語法雖然隱藏了語法樹的細節,但其實它提供了很多類似類,函數的概念來幫助我們查找相關'語法'。
依舊是這段代碼為例子
var param = location.hash.split("#")[1];
document.write("Hello " + param + "!");
上文我們已經建立好了查詢的數據庫,現在我們分別來看如何查找sink,source,以及怎樣將它們關聯起來。
我也是看它的文檔:https://help.semmle.com/QL/learn-ql/javascript/introduce-libraries-js.html 學習的,它提供了很多方便的函數,我沒有仔細看。我的查詢語句都是基于語法樹的查詢思想,可能官方已經給出了更好的查詢方式,所以看看就行了,反正也能用。
查詢 document.write
import javascript
from Expr dollarArg,CallExpr dollarCall
where dollarCall.getCalleeName() = "write" and
dollarCall.getReceiver().toString() = "document" and
dollarArg = dollarCall.getArgument(0)
select dollarArg
這段語句的意思是查找document.write,并輸出它的第一個參數

查找 location.hash.split
import javascript
from CallExpr dollarCall
where dollarCall.getCalleeName() = "split" and
dollarCall.getReceiver().toString() = "location.hash"
select dollarCall
查找location.hash.split并輸出

數據流分析
接著從sink來找到source,將上面語句組合下,按照官方的文檔來就行
class XSSTracker extends TaintTracking::Configuration {
XSSTracker() {
// unique identifier for this configuration
this = "XSSTracker"
}
override predicate isSource(DataFlow::Node nd) {
exists(CallExpr dollarCall |
nd.asExpr() instanceof CallExpr and
dollarCall.getCalleeName() = "split" and
dollarCall.getReceiver().toString() = "location.hash" and
nd.asExpr() = dollarCall
)
}
override predicate isSink(DataFlow::Node nd) {
exists(CallExpr dollarCall |
dollarCall.getCalleeName() = "write" and
dollarCall.getReceiver().toString() = "document" and
nd.asExpr() = dollarCall.getArgument(0)
)
}
}
from XSSTracker pt, DataFlow::Node source, DataFlow::Node sink
where pt.hasFlow(source, sink)
select source,sink

將source和sink輸出,就能找到它們具體的定義。
我們找到查詢到的樣本

可以發現它的回溯是會根據變量,函數的返回值一起走的。
當然從source到sink也不可能是一馬平川的,中間肯定也會有阻擋的條件,ql官方有給出解決方案。總之就是要求我們更加細化完善ql查詢代碼。
接下來放出幾個查詢還不精確的樣本,大家可以自己嘗試如何進行查詢變得精確。
var custoom = location.hash.split("#")[1];
var param = '';
param = " custoom:" + custoom;
param = param.replace('<','');
param = param.replace('"','');
document.write("Hello " + param + "!");
quora = {
zebra: function (apple) {
document.write(this.params);
},
params:function(){
return location.hash.split('#')[1];
}
};
quora.zebra();
最后
CodeQL將語法樹抽離出來,提供了一種用代碼查詢代碼的方案,更增強了基于數據分析的靈活度。唯一的遺憾是它并沒有提供很多查詢漏洞的規則,它讓我們自己寫。這也不由得讓我想起另一款強大的基于語義的代碼審計工具fortify,它的規則庫是公開的,將這兩者結合一下說不定會有不一樣的火花。
Github公告說將用它來搜索開源項目中的問題,而作為安全研究員的我們來說,也可以用它來做類似的事情?
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1078/