作者: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的擴展可以方便我們看代碼

  • 然后到擴展中心配置相關參數

image-20191116223514188

4. image-20191116223649659

  • cli填寫下載的分析程序路徑就行了,windows可以填寫codeql.cmd
  • 其他地方默認就行

建立數據庫

以JavaScript為例,建立分析數據庫,建立數據庫其實就是用分析程序來分析源碼。到要分析源碼的根目錄,執行codeql database create jstest --language=javascript

image-20191117111305487

接下來會在該目錄下生成一個jstest的文件夾,就是數據庫的文件夾了。

接著用vscode打開之前下載的ql庫文件,在ql選擇夾中添加剛才的數據庫文件,并設置為當前數據庫。

image-20191117111940680

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

看它的庫文件,它基本把JavaScript中用到的庫,或者其他語言的定義語法都支持了。

image-20191117113240934

輸出一段hello world試試?

image-20191118130324959

語義分析查找的原理

剛開始接觸ql語法的時候可能會感到它的語法有些奇怪,它為什么要這樣設計?我先說說自己之前研究基于JavaScript語義分析查找dom-xss是怎樣做的。

首先一段類似這樣的javascript代碼

var param = location.hash.split("#")[1];
document.write("Hello " + param + "!");

常規的思路是,我們先找到document.write函數,由這個函數的第一個參數回溯尋找,如果發現它最后是location.hash.split("#")[1];,就尋找成功了。我們可以稱document.writesink,稱location.hash.splitsource。基于語義分析就是由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方便查看

最終就得到了如下一個樹結構

image-20191118131714042

這些樹結構的一些定義可以參考: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"
  }
}

在找到了這些sinksource后,再進行正向或反向的回溯分析。回溯分析也會遇到不少問題,如何處理對象的傳遞,參數的傳遞等等很多問題。之前也基于這些設計寫了一個在線基于語義分析的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,并輸出它的第一個參數

image-20191118134431944

查找 location.hash.split

import javascript

from CallExpr dollarCall
where dollarCall.getCalleeName() = "split" and
    dollarCall.getReceiver().toString() = "location.hash"
select dollarCall

查找location.hash.split并輸出

image-20191118134554200

數據流分析

接著從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

image-20191118134945286

將source和sink輸出,就能找到它們具體的定義。

我們找到查詢到的樣本

image-20191118135549113

可以發現它的回溯是會根據變量,函數的返回值一起走的。

當然從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公告說將用它來搜索開源項目中的問題,而作為安全研究員的我們來說,也可以用它來做類似的事情?


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1078/