作者:书签收藏家
来源:https://paper.seebug.org/1324/
前言
目前CodeQL依然是一套不够完善、需要不断改进的代码扫描工具,与市面上成熟的代码扫描工具仍有较大差距。网上CodeQL相关教程大部分只是官方Hello World教程的汉化版,少部分有价值的文章也只将目光集中在CodeQL语法和QL文件的编写上。这对于分析前不需要进行编译的语言(JavaScript、TypeScript、Python)倒也没有太大影响。但在其他语言中,正确、完整地生成数据库(本文所有数据库均指由CodeQL生成的数据库)与QL文件的编写拥有同等重要的地位,为了评估CodeQL的潜力以及弥补现有资料的不足因而有了本篇文章。
主要内容
本文包含了大量使用CodeQL生成数据库(database)的内容,基本不包括QL文件的编写。同时也涉及到了JSP文件编译、Apache Ant、jar包反编译与再编译等内容。
阅前建议
在阅读本文前建议阅读CodeQL官方教程,尤其是其中关于生成数据库的部分。
基本原理
正常使用CodeQL分析Java项目的过程可分为两部分:
- 根据项目代码,通过代码编译过程生成数据库
- 使用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所支持的,也无法正常生成数据库。
导致无法正常生成数据库的常见原因有:
- 项目缺少依赖或代码出错
- 项目编译命令或编译配置出错
- 编译过程被跳过(上一次编译的缓存未清除等)
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编译器和编译方式
在该项目编译的过程中导致编译错误的原因有以下三类:
- 项目中jsp代码出错,无法被编译
- 缺少依赖(dead code相关的依赖)
- 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进行反编译与再编译,从而减少需要分析的数据量,提高分析速度。
结果对比
本次示例项目为闭源程序,不存在直接编译的可能,根据被分析的代码范围的不同,产生了三种结果。
三种代码范围如下:
- 只编译jsp
- 编译jsp和jsp直接引用的jar(包括全部私有jar与少量第三方jar)
- 编译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包编写规则。