作者:書簽收藏家
本文為作者投稿,Seebug Paper 期待你的分享,凡經采用即有禮品相送! 投稿郵箱:paper@seebug.org

前言

目前CodeQL依然是一套不夠完善、需要不斷改進的代碼掃描工具,與市面上成熟的代碼掃描工具仍有較大差距。網上CodeQL相關教程大部分只是官方Hello World教程的漢化版,少部分有價值的文章也只將目光集中在CodeQL語法和QL文件的編寫上。這對于分析前不需要進行編譯的語言(JavaScript、TypeScript、Python)倒也沒有太大影響。但在其他語言中,正確、完整地生成數據庫(本文所有數據庫均指由CodeQL生成的數據庫)與QL文件的編寫擁有同等重要的地位,為了評估CodeQL的潛力以及彌補現有資料的不足因而有了本篇文章。

主要內容

本文包含了大量使用CodeQL生成數據庫(database)的內容,基本不包括QL文件的編寫。同時也涉及到了JSP文件編譯、Apache Ant、jar包反編譯與再編譯等內容。

閱前建議

在閱讀本文前建議閱讀CodeQL官方教程,尤其是其中關于生成數據庫的部分

基本原理

正常使用CodeQL分析Java項目的過程可分為兩部分:

  1. 根據項目代碼,通過代碼編譯過程生成數據庫
  2. 使用QL文件對數據庫進行分析、生成bqrs文件

在使用CodeQL生成Java項目的數據庫時,如果沒有指定'--command',CodeQL會根據平臺的不同,調用./java/tools/autobuild.cmd或 ./java/tools/autobuild.sh對項目進行分析。如果該項目的編譯工具為Gradle、Maven或Ant,且能找到相應的配置文件。程序就會進入相應的流程,調用相關的編譯指令對項目進行編譯。CodeQL會收集項目編譯過程中產生的信息,并以此生成數據庫。

如果不屬于Gradle、Maven、Ant中任意一種,則報錯退出。

[build-err] ERROR: Could not detect a suitable build command for the source checkout.

對于使用其他方式(例如Eclipse)編譯的項目,只需要指定'--command',將對應的編譯指令傳遞給CodeQL,也可以正常地生成數據庫。

因此我們完全可以編寫一個通用的編譯腳本來生成數據庫。

#/bin/sh
# sh sh.sh ./ "-cp ./ -encoding utf-8"
cur_dir=$(pwd)
javac="/usr/bin/javac"

getdir() {
    method=${2}
    filetype=${3}
    for element in $(ls ${1}); do
        dir_or_file=${1}"/"${element}
        if [ -d ${dir_or_file} ]; then
            getdir ${dir_or_file} "${method}" "${filetype}"
        else
            if [ "${dir_or_file##*.}"x = ${filetype}x ]; then
                ${method} ${dir_or_file}
            fi
        fi
    done
}

if [ -d ${1} ]; then
    src=${1}
else
    if [ -f ${1} ]; then
        ${javac} ${2} ${1}
    fi
    exit
fi

getdir ${src} "rm -f" "class"
if [ -n "${2}" ]; then
    getdir ${src} "${javac} ${2}" "java"
else
    getdir ${src} "${javac}" "java"
fi

CodeQL只收集編譯過程中產生的信息,而不關心代碼是怎么被編譯的,或者收集到的信息是否完整。即使項目在編譯過程中中斷、出錯,或者部分代碼沒有被編譯,CodeQL也能正常對已收集到的信息進行正確處理。同時,只要當前項目無法產生編譯信息,即使項目的編譯方式是被CodeQL所支持的,也無法正常生成數據庫。

導致無法正常生成數據庫的常見原因有:

  1. 項目缺少依賴或代碼出錯
  2. 項目編譯命令或編譯配置出錯
  3. 編譯過程被跳過(上一次編譯的緩存未清除等)

CodeQL需要代碼編譯過程中的信息,而不關注代碼編譯后生成的字節碼文件。因此無法通過“將上一次編譯好的字節碼文件拷貝到原項目中”的方式來欺騙CodeQL生成數據庫。對于在編譯過程中沒有編譯到的代碼也不會被存入數據庫。即使項目的編譯方式是被CodeQL所支持的,要使用CodeQL對其進行分析往往也需要重寫配置文件。

以靶場項目bodgeit為例。項目使用Apache Ant進行編譯,項目代碼包括外部依賴庫(./lib),Java代碼(./src),Web代碼和資源(./root)。照理說這個項目應該是符合使用CodeQL分析的要求的,只需要按照官方教程操作即可。但在實際上,按照官方教程創建數據庫后使用CodeQL官方規則(./java/ql/src/Security/CWE)分析后,在這套程序中僅僅發現了三處漏洞:

"Hard-coded credential in sensitive call","Using a hard-coded credential in a sensitive call may compromise security.","error","Hard-coded value flows to [[""sensitive call""|""relative:///src/com/thebodgeitstore/selenium/tests/FunctionalTest.java:134:33:134:42""]].","/src/com/thebodgeitstore/selenium/tests/FunctionalTest.java","134","33","134","42"
"Hard-coded credential in sensitive call","Using a hard-coded credential in a sensitive call may compromise security.","error","Hard-coded value flows to [[""sensitive call""|""relative:///src/com/thebodgeitstore/selenium/tests/FunctionalTest.java:141:33:141:42""]].","/src/com/thebodgeitstore/selenium/tests/FunctionalTest.java","141","33","141","42"
"Hard-coded credential in sensitive call","Using a hard-coded credential in a sensitive call may compromise security.","error","Hard-coded value flows to [[""sensitive call""|""relative:///src/com/thebodgeitstore/selenium/tests/FunctionalTest.java:145:30:145:39""]].","/src/com/thebodgeitstore/selenium/tests/FunctionalTest.java","145","30","145","39"

將數據庫根目錄下的src.zip文件解壓后,造成CodeQL無法從bodgeit中找到漏洞的原因就很清楚了。可以看到其中只包含了Java代碼(./src)部分,缺少了外部依賴庫(./lib)以及項目中的jsp文件。對數據庫文件進行分析結果也是一致的。

src.zip目錄結構:

BODGEIT\ROOT
└───exp
    └───bodgeit-1.4.0
        └───src
            └───com
                └───thebodgeitstore
                    ├───search
                    │       AdvancedSearch.java
                    │       SearchResult.java
                    │
                    ├───selenium
                    │   └───tests
                    │           FunctionalTest.java
                    │           FunctionalZAP.java
                    │
                    └───util
                            AES.java

應當認識到,一個Java項目的編譯與CodeQL所需要的編譯并不是完全對等。Java項目的編譯需要保證編譯后項目中包含了所需要的字節碼和代碼文件,而CodeQL需要編譯過程。使用項目自帶的編譯將導致:

  • CodeQL無法分析已經預先編譯好jar包
  • CodeQL無法分析在運行時才被編譯的jsp代碼

bodgeit的漏洞代碼集中在jsp文件中,如果不修改編譯配置的話,CodeQL只能對其中的java代碼進行分析,無法分析存在漏洞的jsp代碼。

此外,bodgeit自帶的編譯配置沒有清除上一次編譯產生的緩存文件。按照官方教程重復使用該項目生成數據庫,在第二次生成數據庫時編譯過程會被跳過,導致無法正常生成數據庫。

牢記一點,CodeQL透過編譯過程生成數據庫,CodeQL無法分析未被編譯的代碼

分析閉源程序

如果你已經仔細閱讀過以上內容,對于如何使用CodeQL分析閉源Java程序相必已胸有成竹。以下內容僅僅只是提供一些解決問題的細節。

示例的項目信息

該項目是一套商用閉源程序,主要提供Web服務,使用Apache Tomcat作為容器。

除去后端依賴服務后,程序代碼可分為:

  • Tomcat相關程序、代碼和jar包
  • 第三方jar
  • 私有jar
  • Jsp文件
  • 其他靜態資源文件

選擇Java反編譯工具

相信很多人第一時間想起的就是jd-gui,或者說jd-core。對比了cfr、procyon、jd-core。在實際使用中,將procyon反編譯出的Java代碼再編譯回去產生的錯誤最少,因此選擇使用procyon進行反編譯。

選擇Java編譯器和編譯方式

在該項目編譯的過程中導致編譯錯誤的原因有以下三類:

  1. 項目中jsp代碼出錯,無法被編譯
  2. 缺少依賴(dead code相關的依賴)
  3. Java反編譯不夠完美導致出錯

其中1、2對CodeQL的分析幾乎沒有影響,3會導致部分代碼無法被分析,但目前還沒有辦法完全解決,對于大型項目手動修復代碼也基本不可能實現。

編譯錯誤將會導致CodeQL無法分析對應的Java代碼。為了減少編譯錯誤需要使用容錯率較高的ecj,而不是一編譯出錯就終止的javac。

可以使用bash腳本去編譯代碼,但使用成熟的工具會更加方便。Tomcat使用Ant編譯jsp,并且還提供了編譯腳本,稍微修改一下就能使用。這里使用Ant進行編譯。

確定被分析的代碼范圍

想要完整地生成一個完整的數據庫,或許應該將所有被引用的第三方jar包、甚至Tomcat的jar包納入分析范圍。但實際上這種做法幾乎不可行,具體見下一節。

如前面說的,生成數據庫和編寫QL同樣重要,在生成數據庫前需要根據QL去選擇需要分析范圍。

這次使用的是CodeQL官方規則(./java/ql/src/Security/CWE),這份規則的sink集中在java和javax兩個包中,也就是基本只對Java原生方法進行分析,并且沒有對第三方jar包進行額外的支持。因此使用這份規則應當將Java原生方法以外的包納入分析范圍。假設規則對第三方的包(例如com.example.www)進行了適配,在生成數據庫的時候就可以不對com.example.www進行反編譯與再編譯,從而減少需要分析的數據量,提高分析速度。

結果對比

本次示例項目為閉源程序,不存在直接編譯的可能,根據被分析的代碼范圍的不同,產生了三種結果。

三種代碼范圍如下:

  1. 只編譯jsp
  2. 編譯jsp和jsp直接引用的jar(包括全部私有jar與少量第三方jar)
  3. 編譯jsp、全部的私有jar和全部的第三方jar(不包括Tomcat相關的jar)

2中的jsp直接引用的jar是通過腳本自動完成的。

在確定最大錯誤次數(編譯jsp時,引入Tomcat相關jar,不引入其他jar)和最小錯誤次數(編譯jsp時,引入全部jar)后,分別單獨去掉每個jar后對jsp進行編譯,從而確定該jar包是否被jsp直接引用。

近200個被jar包中,除了私有jar,僅有兩個第三方jar(commons-lang、poi-scratchpad)被jsp直接引用,代碼量減少了近兩個數量級。

代碼范圍 范圍1 范圍2 范圍3
編譯錯誤數量 14條 172條 3萬多條
數據庫大小 100MB 350MB 2.5GB
緩存文件大小 50MB 150MB 6GB
漏洞數量 757條 20975條

在普通電腦性能下,范圍1、2在一個小時內就得到了結果,但范圍3在跑了10個小時后連一條規則都跑不完(java/ql/src/Security/CWE/CWE-022/TaintedPath.ql)。這除了因為基于抽象語法樹的分析在代碼量增加時,路徑數量會以指數上升外,也因為CodeQL目前沒有多核優化,使得“一核有難,十核圍觀”。每次分析都會重新生成緩存文件也使得大項目的分析速度異常緩慢。

整體而言,對于大型范圍2效果最好,兼顧了性能,并發現了盡可能多的漏洞。對于小項目可以將所有代碼一起進行分析,QL文件的編寫會更為簡單。不對間接引用的第三方jar包進行分析是基于目前性能考量的一種無奈之舉,這將導致一部分漏洞無法被漏洞。因此分析大型項目建議針對常用的、代碼量較大第三方jar包編寫規則。


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