

CodeQl 从0到0.1
source link: https://blog.szfszf.top/article/59/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

CodeQl 从0到0.1
2022年03月, 12 views, #代码审计 #codeql #static analysis本文记录下CodeQL使用和学习过程中积累的知识。首先是CodeQl通用的知识比如各种query的写法,CodeQL CLI的使用,还有CodeQl For JavaScript库的相关使用。
CodeQl General
Metadata for CodeQl queries
描述ql文件信息的注释,位于ql最上面。(注@id中的字符需全部小写)
/**
* @name backExtractBlockerFromPvn
* @description backExtractBlockerFromPvn
* @kind path-problem
* @problem.severity warning
* @tags security
* @id js/back-extract-blocker-from-pvn
*/
query kind
@kind
指示了本query的类型,常用有两种类型
- Alert queries: queries that highlight issues in specific locations in your code.
- Path queries: queries that describe the flow of information between a source and a sink in your code.
Alert query
用于展示CodeQl查询出来的相关node信息和描述文字。Path query
用于展示Codeql查询出从source流向sink的完整路径信息。在使用CLI进行查询的时候,Alert query
必须在meta信息中标明@kind
为problem
,而Path query
则为path-problem
,使用vscode不包含meta信息对于path query
会报个错。
其他还有@kind
为diagnostic
和metric
的Diagnostic query
和Summary query
,使用较少。
Write a query
不同的query类型对于result的格式不太相同,在使用CLI进行查询的时候,必须满足对应格式才能得到结果。
alert query
对于Alert query,select 出的结果分为两部分,element和string。element代表需要展示的节点,string为这个节点描述信息。
比如对下面js文进行分析,select出可能会流向exec函数的参数。
const exec = require("child_process").exec
function func1(data){
exec(data);
}
var input = userInput();
func1(input);
Alert query
/**
* @name testForAlertQuery
* @kind problem
* @problem.severity warning
* @tags correctness
* @id js
*/
import javascript
import DataFlow
from ParameterNode pn, SourceNode exec
where exec = DataFlow::moduleMember("child_process", "exec") and
pn.getASuccessor*() = exec.getACall().getArgument(0)
select pn, "Function " + pn.getParameter().getEnclosingFunction().getName()
+ " parameter " + pn.getName() + " flows to exec function."
其中pn就是想要select出来的会流向exec函数的函数参数,后面的string详细地描述了哪个函数(func1)的哪个参数(data)会流向exec。
path query
对于Path query,select出来的结果分为四个部分。element,source,sink,string
,其中element
和string
和Alert query 是相同的,而source和sink就分别表示数据流分析中的souce和sink对应得PathNode
/**
* @name testForPathQuery
* @kind path-problem
* @problem.severity warning
* @tags correctness
* @id js
*/
import javascript
import DataFlow
import PathGraph
class ExecConfiguration extends Configuration {
ExecConfiguration() { this = "ExecConfiguration" }
override predicate isSource(DataFlow::Node source) {
exists(CallExpr pn |
pn.getCalleeName() = "userInput" |
source.asExpr() = pn
)
}
override predicate isSink(DataFlow::Node sink) {
DataFlow::moduleMember("child_process", "exec").getACall().getArgument(0) = sink
}
}
from ExecConfiguration cfg, PathNode source, PathNode sink
where cfg.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "User input flows to exec function."
结果中还会带有source到sink的路径。

extra element
无论是alert query还是path query目前都只select出了一个element,虽然可以在string中附加内容,但是很多时候我们更希望能拿到element数据,这样可以直接获得location等信息。
CodeQl允许在string字段中使用$@
作为占位符,在string中每加一个$@
占位符,我们就可以在select语句后面依次添加一对element/string
,其中string代表显示在展示message中替换占位符的内容,element为其对应得节点。
比如我们修改上面得alert query
/**
* @name testForAlertQuery
* @kind problem
* @problem.severity warning
* @tags correctness
* @id js
*/
import javascript
import DataFlow
from ParameterNode pn, SourceNode exec
where exec = DataFlow::moduleMember("child_process", "exec") and
pn.getASuccessor*() = exec.getACall().getArgument(0)
select pn, "Function $@ parameter $@" + " flows to exec function.",
pn.getParameter().getEnclosingFunction(), pn.getParameter().getEnclosingFunction().getName(),
pn.getParameter(), pn.getName()
结果中就能找到额外的element信息。

CodeQL CLI
CodeQl CLI是不开源的用于解析数据库执行ql查询的命令,使用CLI可以创建query数据库和批量测试ql文件。安装CLI也很简单,下载对应版本并添加环境变量即可。然后我们需要下载Codeql库ql文件,将其放在CodeQl CLI同级目录就可以,执行CLI命令时默认会搜索同级目录(及子目录)下所有的QL packs。
创建数据库时,对于脚本语言JavaScript和Python等,比较简单
codeql database create --language=javascript --source-root <folder-to-extract> databaseName
对于编译型语言如java,需要指定--command
参数来编译源代码。
codeql database analyze <database> --format=<format> --output=<output> <queries>
QL Packs
QL Packs 是很多ql文件以某种结构组成的一个库,CodeQl官方仓库提供了C/C++, C#, Java, JavaScript, Python等的库QL Packs,我们也可以打包有用的ql文件作为库以供他人调用。
QL Packs 需要在其根目录下存在一个qlpack.yml
文件,这个文件描述了这个pack的语言类型、和其他pack的依赖关系等。所有 CodeQL CLI 加载了的ql packs都可以以qlpack.yml
为根目录,按目录结构的形式import进来。比如下面官方的例子。

qlpack.yml
内容参数可以看这里。
普通的query库,libraryPathDependencies
用于指定依赖的packs,codeql/javascript-all
是官方javascript库packs的名字。
name: my-queries
version: 0.0.0
libraryPathDependencies: codeql/javascript-all
QL test
在实现我们的库时,通常需要写一些test,CodeQl提供了相关的功能,Codeql会批量执行对应ql文件,对比expected文件结果。
首先我们要创建一个test QL pack,将我们的test文件放在里面,其中qlpack.yml
需要包含以下内容
name: <name-of-test-pack>
version: 0.0.0
libraryPathDependencies: <codeql-libraries-and-queries-to-test>
extractor: <language-of-code-to-test>
其中libraryPathDependencies
就是你这个test QL pack所依赖的pack,也可以说是需要测试pack。
然后在这个test pack中创建子文件夹,其中需要包含.qlref
和.expected
以及测试代码文件文件,.qlref
文件内容为要执行的ql文件位置,.expected
文件内容为执行ql文件后的预期返回内容。
执行测试命令时,codeql会先将该文件夹下的测试代码创建数据库,然后执行.qlref
文件中指向的ql文件将结果与.expected
结果进行比对判断test是否成功。
值得注意的是,.qlref
和.expected
文件名必须一样,如果ql文件就放在了同一目录下,那么就可以没有.qlref
文件,但是ql文件的名字也需要一致。
一个test例子。

CodeQl For JavaScript
Basic library for javascript
在这里介绍一些常用的javascript官方库,主要是数据流分析相关库,也是我们做安全测试主要使用的部分。
data flow node
这里给出了一些DataFlow上的Node,DataFlow上的一个Node一般与AST上的一个Node对应。CodeQl官方库也给出了简便从DataFlow上获取全局变量或者import模块的方法。
DataFlow::globalVarRef("document")
DataFlow::moduleMember("fs", "readFile")
local data flow
CodeQl中使用过程内数据流分析很简单。DataFlow::Node
提供了两个predicate getAPredecessor
和getASuccessor
,可以获取过程内数据流分析中流向这个Node或这个Node流向的Node。因为返回的也是一个DataFlow::Node
所以可以利用nd.getASuccessor*()
或nd.getASuccessor+()
链式调用找到后续所有Node(*
0-,+
1-)。
global data flow
CodeQL for JavaScript 提供了Configuration类对全局数据流分析(过程间数据流分析)进行配置。在它的定义文件semmle\javascript\dataflow\Configuration.qll
最上面的注释中我们可以大概理解它实现的原理。CodeQl实现了一个基于摘要的过程间数据流分析,跟踪过程间变量和部分对象属性的数据流,通过函数摘要的方式跟踪函数调用的数据流。
class MyDataFlowConfiguration extends DataFlow::Configuration {
MyDataFlowConfiguration() { this = "MyDataFlowConfiguration" }
override predicate isSource(DataFlow::Node source) { /* ... */ }
override predicate isSink(DataFlow::Node sink) { /* ... */ }
// optional overrides:
override predicate isBarrier(DataFlow::Node nd) { /* ... */ }
override predicate isBarrierEdge(DataFlow::Node pred, DataFlow::Node succ) { /* ... */ }
override predicate isBarrierGuard(BarrierGuardNode guard) { /* ... */ }
override predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ) { /* ... */ }
}
isSource
和isSink
predicate用于约束数据流分析的起点和终点,上文已有例子。
通常输入安全措施有两种,sanitization、validation即过滤和验证。
// sanitization
var data = sanitize(input);
// validation
if(checkInput(input)){
...
}
if(input === "whoami"){
...
}
var arr = ["1", 2];
if(arr.includes(input)){
...
}
我们可以用isBarrier
实现对过滤函数的处理,isBarrier
函数参数为需要阻断数据流的Node,意味着满足下面条件的Node即使数据流传到了它也不会继续向下传递。
比如对于上面的sanitize函数的过滤,我们可以实现一个下面这种的barrier,表示所有以sanitize函数的调用节点都不会向后传递数据流。(注意,后面的isBarrierGuard
是利用父isBarrier执行的,直接override不执行父 isBarrier predicate会导致配置isBarrierGuard
失效)
override predicate isBarrier(DataFlow::Node nd) {
super.isBarrier(nd)
or
nd.(CallNode).getCalleeName() = "sanitized1"
}
对于输入验证相关,可以使用isBarrierGuard
来模拟有条件的阻断数据流。
isBarrierGuard
的官方定义如下。参数BarrierGuardNode类实例需要定义一个限制方法,来告诉CodeQl在某个条件语句遇到这种情况在then或者else分支种限制某个Node的传递。
/**
* Holds if data flow node `guard` can act as a barrier when appearing
* in a condition.
*
* For example, if `guard` is the comparison expression in
* `if(x == 'some-constant'){ ... x ... }`, it could block flow of
* `x` into the "then" branch.
*/
要实现一个BarrierGuardNode也很简单,定义一个继承自BarrierGuardNode的类,然后需要实现一个名为 blocks 的 predicate。它含有两个参数,意味着当使用这个 guard Node 作为条件语句时,条件返回为outcome的情况下,会在then分支过滤e对应的Node。
/**
* if(checkInput(input)){
* ...
* }
* 检查当一个CallNode作为条件语句结果时,如果调用的函数名为CheckInput,
* 那么函数返回true的话,就在then分支中过滤这个callNode的第一个参数。
*/
class CheckInputBarrierGuardNode extends BarrierGuardNode, CallNode {
CheckInputBarrierGuardNode() { this.getCalleeName() = "CheckInput" }
override predicate blocks(boolean outcome, Expr e) {
outcome = true and
e = getArgument(0).asExpr()
}
}
/**
* if(checkInput(input)){}
* if(input === "whoami"){}
* var arr = ["1", 2];
* if(arr.includes(input)){}
* 检查当一个membership test作为条件语句条件时,membership test可以为静态值比较的EqualityTest
* 也可以是 Array include等,具体可以看实现文件semmle\javascript\MembershipCandidates.qll
*/
class StaticValueBarrierGuardNode extends BarrierGuardNode {
MembershipCandidate candidate;
StaticValueBarrierGuardNode() { this = candidate.getTest() }
override predicate blocks(boolean outcome, Expr e) {
candidate = e.flow() and candidate.getTestPolarity() = outcome
}
}
然后再配置Configuration
override predicate isBarrierGuard(BarrierGuardNode guard) {
guard instanceof CheckInputBarrierGuardNode
or
guard instanceof StaticValueBarrierGuardNode
}
还有一种实现的方法,就是继承自AdditionalBarrierGuardNode
,就不用配置isBarrierGuard
了。原理也很简单看下源码就知道了。
class CheckInputBarrierGuardNode1 extends AdditionalBarrierGuardNode, CallNode {
CheckInputBarrierGuardNode1() { this.getCalleeName() = "CheckInput" }
override predicate blocks(boolean outcome, Expr e) {
outcome = true and
e = getArgument(0).asExpr()
}
override predicate appliesTo(Configuration cfg) { any() }
}
isBarrierEdge
和isAdditionalFlowStep
类似,前者是阻断对某类Node到某类Node的数据流,而后者是额外添加数据流的连接。Configuration.qll
文件中flowStep
predicate 记录了CodeQl过程间分析定义的step。
我们在分析一些复杂的项目的时候,可能会存在一些动态函数的数据流断掉了,那么可以让我们手动加上去。
let data = argFunc(input);
exec(data);
下面代码的意思是将pred
和succ
两个Node进行连接,数据流就连起来了。
override predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ) {
exists(CallNode call |
call.getCalleeName() = "argFunc" and
pred = call.getArgument(0) and
succ = call
)
}
下面是整体的代码。
/**
* @name inter-procedual data flow analysis
* @kind path-problem
* @problem.severity warning
* @tags security
* @id js
*/
import javascript
import DataFlow
import PathGraph
class MyConfiguration extends Configuration {
MyConfiguration() { this = "MyConfiguration" }
override predicate isSource(DataFlow::Node source) {
exists(CallExpr pn | pn.getCalleeName() = "easySource" | source.asExpr() = pn)
}
override predicate isBarrier(DataFlow::Node nd) {
super.isBarrier(nd)
or
nd.(CallNode).getCalleeName() = "sanitized1"
}
override predicate isSink(DataFlow::Node sink) {
DataFlow::moduleMember("child_process", "exec").getACall().getArgument(0) = sink
}
override predicate isBarrierGuard(BarrierGuardNode guard) {
guard instanceof CheckInputBarrierGuardNode
or
guard instanceof StaticValueBarrierGuardNode
}
override predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ) {
exists(CallNode call |
call.getCalleeName() = "argFunc" and
pred = call.getArgument(0) and
succ = call
)
}
}
class CheckInputBarrierGuardNode1 extends AdditionalBarrierGuardNode, CallNode {
CheckInputBarrierGuardNode1() { this.getCalleeName() = "CheckInput" }
override predicate blocks(boolean outcome, Expr e) {
outcome = true and
e = getArgument(0).asExpr()
}
override predicate appliesTo(Configuration cfg) { any() }
}
class CheckInputBarrierGuardNode extends BarrierGuardNode, CallNode {
CheckInputBarrierGuardNode() { this.getCalleeName() = "CheckInput" }
override predicate blocks(boolean outcome, Expr e) {
outcome = true and
e = getArgument(0).asExpr()
}
}
class StaticValueBarrierGuardNode extends BarrierGuardNode {
MembershipCandidate candidate;
StaticValueBarrierGuardNode() { this = candidate.getTest() }
override predicate blocks(boolean outcome, Expr e) {
candidate = e.flow() and candidate.getTestPolarity() = outcome
}
}
from MyConfiguration cfg, PathNode source, PathNode sink
where cfg.hasFlowPath(source, sink)
select sink.getNode(), source, sink, sink.toString()
global taint tracking
taint tracking 和 data flow 类似,可以看到每个predicate都有相互对应的,其使用方法也是类似的。
class MyTaintTrackingConfiguration extends TaintTracking::Configuration {
MyTaintTrackingConfiguration() { this = "MyTaintTrackingConfiguration" }
override predicate isSource(DataFlow::Node source) { /* ... */ }
override predicate isSink(DataFlow::Node sink) { /* ... */ }
// optional overrides:
override predicate isSanitizer(DataFlow::Node nd) { /* ... */ }
override predicate isSanitizerEdge(DataFlow::Node pred, DataFlow::Node succ) { /* ... */ }
override predicate isSanitizerGuard(SanitizerGuardNode guard) { /* ... */ }
override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) { /* ... */ }
}
查看官方库的代码semmle\javascript\dataflow\TaintTracking.qll
,taint tracking在data flow的基础上,添加了对于字符串数组等操作相关的additionalStep,它抽象出了一个SharedTaintStep类,只要继承自它并实现了uriStep/persistentStorageStep/heapStep/arrayStep/viewComponentStep/stringConcatenationStep/stringManipulationStep/serializeStep/deserializeStep/promiseStep
等predicate,就可以方便的对taintTracking扩展额外的step。一个官方实现的例子在Arrays.qll
中,可以看到对数组一些操作进行了model,实现了taint数据在数组操作之间的流动。

继承了AdditionalSanitizerGuardNode
类也实现了很多guard过滤的方法,这里就包含了我们上面MembershipTestSanitizer
还有empty限制x.length === "0"
in限制if(x in o)
正则限制等,我们可以通过看这里面的代码学习这么写guard。
flow labels
对于数据流中的数据,我们可以在数据流传播的过程中将数据打上标签,这样的话我们可以实现更加复杂的数据流分析。
官方实现了两种 flow label,dataflow 的 data
,和taint tracking 的 taint
。官方库中对于additionalStep的实现就是保持pred和succ的flow label,Barriers实现方法就是将node的data类型的label去掉,Sanitizers就是将taint类型的label去掉。

有时候我们也需要对数据流中的数据打上更多的标签,文档中举了一个很形象的例子,当我们要过滤某个path时,为了防止目录穿越,我们需要确保输入内容即不能是绝对路径,也不能含有..
。这两个过滤应该分别作为一个guard或者sanitizer在数据流上限制数据,且需要两个同时存在时才能保证安全。那么我们就可以在每经过一种guard或sanitizer时修改标签,在isSink再检查它的标签就可以了。
下面我实现了一个对于下面代码的数据流分析,数据经过sanitized1函数应该被过滤,但是数据在后面进行了urldecode,此后的数据仍然可能是可控的。
const exec = require("child_process").exec
var input = easySource();
var data = sanitized1(input)
exec(data)
var data1 = decodeURI(data)
exec(data1)
所以我定义了两种label,经过sanitized1函数的时候对数据打上SantizedLabel
label,经过urldecode函数的时候将SantizedLabel
转换成UrlDecodeLabel
label.
/**
* @name inter-procedual data flow analysis with label
* @kind path-problem
* @problem.severity warning
* @tags security
* @id js
*/
import javascript
import DataFlow
import PathGraph
class UrlDecodeLabel extends DataFlow::FlowLabel {
UrlDecodeLabel() { this = "UrlDecode" }
}
class SantizedLabel extends DataFlow::FlowLabel {
SantizedLabel() { this = "SantizedLabel" }
}
class MyConfiguration extends TaintTracking::Configuration {
MyConfiguration() { this = "MyConfiguration" }
override predicate isSource(DataFlow::Node source) {
exists(CallExpr pn | pn.getCalleeName() = "easySource" | source.asExpr() = pn)
}
override predicate isSanitizerEdge(DataFlow::Node pred, DataFlow::Node succ) {
exists(CallNode call | call.getCalleeName() = "sanitized1" |
pred = call.getArgument(0) and
succ = call
)
}
override predicate isSink(DataFlow::Node sink, FlowLabel lbl) {
DataFlow::moduleMember("child_process", "exec").getACall().getArgument(0) = sink and
not lbl instanceof SantizedLabel
}
override predicate isAdditionalFlowStep(
DataFlow::Node src, DataFlow::Node trg, FlowLabel inlbl, FlowLabel outlbl
) {
exists(CallNode call |
call.getCalleeName() = "decodeURI" and
src = call.getArgument(0) and
trg = call and
inlbl instanceof SantizedLabel and
outlbl instanceof UrlDecodeLabel
)
or
exists(CallNode call |
call.getCalleeName() = "sanitized1" and
src = call.getArgument(0) and
trg = call and
outlbl instanceof SantizedLabel
)
}
}
from MyConfiguration cfg, PathNode source, PathNode sink
where cfg.hasFlowPath(source, sink)
select sink.getNode(), source, sink, sink.toString()
从我们的上面的代码中可以看出,像isSource/isSink/isAdditionalFlowStep
等几乎所有的Configuration的predicate 都是存在含有Flow labels 参数的版本的,看文档或者看源代码就可以得到详细用法。
Debug
-
write some predicate for quick evaluation
-
useful debug tools in
semmle.javascript.explore
Insemmle.javascript.explore.CallGraph
there are 3 predicatescallEdge
,isStartOfCallPath
andisEndOfCallPath
to help us explore(without global taint tacking) in call graph.
semmle.javascript.explore.ForwardDataFlow
and semmle.javascript.explore.BackwardDataFlow
is private class.
- For java, We can use Partial flow
Others
CodeQl for JavaScript 官方库中提供了大量的有用的ql库。比如对于http服务器应用,无论是net库或者是Express都有了一定程度的支持。
比如我们可以直接拿到Express的http输入NodeExpress::RequestInputAccess
作为数据流分析的source,以及一些很方便的router信息。
对于npm包也有响应的解析,我们可以直接拿到Exports出来的函数和参数之类的东西。很多东西翻翻那官方库都能找到。
Recommend
-
28
go-ruleguard Overview analysis -based Go lint...
-
12
CodeQL + XNU From 0 to 1Basic本文属于学习过程中的笔记,基本上是把现有的相关资料整合到一起,阅读已有博客/文章并复现,加入一些自己的想法后记录下来的产物。 build XNU 过程来自某大佬的博客,build xnu...
-
20
Using GitHub code scanning and CodeQL to detect traces of Solorigate and other backdoors Last month, a member of the CodeQL security community contributed
-
11
CodeQL JS/TS Journey之前做过的一些使用CodeQL对JS/TS项目做扫描的笔记。 关于构建数据库过程对于JS/TS的项目来说,CodeQL统一都是 --language=javascript 的参数处理的,而且它主要是扫描,解析,然后构建数据库,...
-
6
CodeQL This open source repository contains the standard CodeQL libraries and queries that power LGTM and the other CodeQL products that GitHub makes available to its...
-
4
How the community powers GitHub Advanced Security with CodeQL queries Secu...
-
6
需要在U-Boot中寻找一组9个远程代码执行漏洞 漏洞点位于memcpy函数 但并非所有调用memcpy函数的都存在漏洞 所以我们需要减少误报率,找到真正存在漏洞的memcpy调用
-
9
郑重声明:文中所涉及的技术、思路和工具仅供以安全为目的的学习交流使用,如果您不同意请关闭该页面!任何人不得将其用于非法用途以及盈利等目的,否则后果自行承担! 关于搭建就不写了,网上太多了,内容大部分都来着互联网,我只是做个...
-
7
知道创宇云防御-浏览器安全检查 浏览器安全检查 创宇盾将在您访问 paper.seebug.org 前对浏览器进行安全检查,请等待 4 秒… · 如果您对安全防护...
-
10
Sharing security expertise through CodeQL packs (Part I)Introducing CodeQL packs to help you codify and share your knowledge of vulnerabilities. ...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK