CodeQL入门

前言

CodeQL 是 GitHub(实际上是Github收购的)推出的一种代码分析引擎和查询语言,用于查找软件中的漏洞和安全问题。它的核心理念是将代码表示成一种可以查询的数据库结构,开发者或研究人员可以像查询数据库一样查询代码的结构和行为,简单来说就是一种可以通过编写 QL 制定规则的静态分析工具(SAST)。

我自己也勉强算是搞SAST的,所以对于现在市面上这些成熟的SAST工具的原理还是蛮好奇的,因为我之前开发TaintScaner的时候其实完全没有了解过其他的SAST的原理,纯粹是看完南大软件分析课后,凭借一种惊人的直觉开发的,因此很多东西都是自己想当然的实现的,完全不知道如今成熟的方案是什么,所以最近准备系统学习一下这些成熟的SAST工具的原理,目前的计划是Codeql、soot和tai-e,后面考虑看一下tabby等等。

原理浅析

当然,CodeQL作为一个不开源的污点分析工具,也没法从源码角度分析它的污点传播原理,我这里主要是看的其他大佬的博客。

codeql 整个漏洞扫描可以分为两部分:

  • AST 数据库创建,通过命令行工具即可
  • 规则编写,类似 SQL 的语法来编写漏洞查询规则

编译出来的数据库中,在db-java目录里装的是代码解析出来的AST,所以说Codeql其实是在AST层上做的污点传播,这一点我还是蛮吃惊的,因为我之前感觉AST层是搞不了污点分析的,因为太复杂了,我还以为大伙都会利用IR来抽象代码的。

环境搭建

CodeQL分引擎和SDK两部分,引擎部分不开源,主要负责解析规则。SDK是开源的,包含很多漏洞规则,也可以自己写漏洞规则进行使用。

引擎地址:https://github.com/github/codeql-cli-binaries/releases,接着配一下环境变量:

接着去安装sdk:https://github.com/github/codeql,把它改名成ql,放在之前引擎的同目录下:

接着我们在vscode安装插件:

添加一下可执行文件的位置:

如果我们想要执行查询,首先需要去待审计源代码的根目录建立分析数据库,我这里随便找了个java靶场,使用下面的命令建立分析数据库:

codeql database create codeqltest --language=java

执行成功你应该可以在这个目录里看到一个codeqltest目录,接着我们在vscode引入这个目录:

接着我们在 CodeQL\ql\java\ql\examples 目录下创建 demo.ql,内容为select "Hello World“,然后利用codeql进行执行,这样就说明我们环境配置好了(我现在的工作区是引擎那个目录,也就是有codeql和ql的那个目录):

规则编写

QL语法

QL语法类似于SQL,大致如下:

from [datatype] var
where condition(var = something)
select var

比如下面的例子,作用就是在 Java 项目中查找所有值为 1 的 int 字面量并输出:

import java // 引入CodeQL类库
 
from int i // 表示所有int类型的数据
where i = 1 // 表示条件:当i等于1时
select i // 输出i

类库

codeql里有几个类库:

  • Method:方法类:Method method表示获取当前项目中所有的方法
  • MethodCall:方法调用类:MethodCall call表示获取当前项目当中的所有方法调用(在某个版本之后,MethodAccess 被重名了成 MethodCall 了)
  • Parameter:参数类:Parameter表示获取当前项目当中所有的参数

比如下面的例子,作用就是查找方法getStudent的名称和类:

import java

from Method method // 表示所有方法
where method.hasName("getStudent") // 表示条件:方法名为getStudent
select method.getName(), method.getDeclaringType() // 输出方法的名称,和方法所属的类名

可以看到出现了两个输出结果,这就说明在类IndexLogic以及IndexDb里都有一个方法getStudent

谓词

where 部分过长时,可以用谓词这个语法,把很长的查询语句封装成函数,其中 predicate 关键词用于声明谓词,exists作为子查询,根据内部的子查询返回true or false,来决定筛选出哪些数据,比如在下面的例子里我们就把判断方法名称是否为getStudent的where部分封装成了一个函数,这个函数就被称作谓词

import java

predicate isStudent(Method method){
	exists(|method.hasName("getStudent"))
} // |操作符返回查询结果的数量,大于0则true

from Method method
where isStudent(method)
select method.getName(), method.getDeclaringType().getName()

Source/Sink

source、sink以及sanitizer想必也不需要介绍了,学静态分析的不可能不知道这三个概念,简单来说,source是可控数据产生点,sink是危险函数,sanitizer是过滤函数。

在我们这个java靶场里,username参数就是一个很明显的source,毕竟这个接口的作用就是利用getStudent方法查询username参数对应的值

在Codeql里,我们可以用sdk来检测这些源点,语法如下:

predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource }

上面这个谓词定义了所有 RemoteFlowSource 类型的节点为“污点源点”,也就是从远程请求传入的用户输入都被认为是不可信的起点,这里的 DataFlow::Node 表示一个数据流节点,下面是一个查询系统里所有源点的demo:

import java
import semmle.code.java.dataflow.FlowSources

predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource }

from DataFlow::Node src
where isSource(src)
select src, "是一个Source点"
可以看到我们之前提过的username参数

那么如何判断汇点呢?至少在这个靶场里,判断sql注入的一个简单的方法是找query方法,也就是寻找sql查询函数,比如下面的谓词里,我们就是定义了所有传给名为 query 方法第一个参数的表达式为 Sink 点:

predicate isSink(DataFlow::Node sink) {
exists(Method method, MethodCall call |
  method.hasName("query")
  and
  call.getMethod() = method and
  sink.asExpr() = call.getArgument(0)
)
}

下面是完整的代码:

import java
import semmle.code.java.dataflow.FlowSources

predicate isSink(DataFlow::Node sink) {
  exists(Method method, MethodCall call |
    method.hasName("query") and
    call.getMethod() = method and
    sink.asExpr() = call.getArgument(0)
  )
}

from DataFlow::Node sink
where isSink(sink)
select sink, "传入 query() 方法的第一个参数是敏感点"

最后我们来写一个完整的demo,判断从Source到Sink是否存在路径。

实操

在CodeQL中存在两种数据流:

  • 本地数据流:本地数据流是指单个方法或可调用函数内的数据流。本地数据流通常比全局数据流更简单、更快速、更精确,并且足以应对许多查询。
  • 全局数据流:全局数据流跟踪整个程序的数据流,因此比局部数据流更强大。然而,全局数据流的精确度低于局部数据流,并且分析通常需要更多的时间和内存。

这里我们当然先学习全局数据流,本地数据流相对而言效果不是太好,我们需要通过实现签名
DataFlow::ConfigSig和应用模块来使用全局数据流库
DataFlow::Global<ConfigSig>

import java
import semmle.code.java.dataflow.DataFlow

module MyFlowConfiguration implements DataFlow::ConfigSig {
  predicate isSource(DataFlow::Node source) {
    ...
  }

  predicate isSink(DataFlow::Node sink) {
    ...
  }
}

module MyFlow = DataFlow::Global<MyFlowConfiguration>;
  • isSource– 定义数据流出的位置。
  • isSink– 定义数据流向何处。
  • isBarrier– 可选,定义数据流被阻止的位置。
  • isAdditionalFlowStep– 可选,添加额外的流程步骤。

如果想使用污点追踪,需要使用TaintTracking:

import java
import semmle.code.java.dataflow.TaintTracking

module MyFlowConfiguration implements DataFlow::ConfigSig {
  predicate isSource(DataFlow::Node source) {
    ...
  }

  predicate isSink(DataFlow::Node sink) {
    ...
  }
}

module MyFlow = TaintTracking::Global<MyFlowConfiguration>;

最后使用使用谓词执行数据流分析:flow(DataFlow::Node source, DataFlow::Node sink)

from DataFlow::Node source, DataFlow::Node sink
where MyFlow::flow(source, sink)
select source, "Data flow to $@.", sink, sink.toString()

因此对于之前的例子,完整的代码如下:

import java
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.dataflow.FlowSources

module SourceToSinkConfig implements DataFlow::ConfigSig {
  predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }

  predicate isSink(DataFlow::Node sink) {
    exists(Method method, MethodCall call |
      method.hasName("query") and
      call.getMethod() = method and
      sink.asExpr() = call.getArgument(0)
    )
  }
}

module SourceToSink = TaintTracking::Global<SourceToSinkConfig>;

from DataFlow::Node source, DataFlow::Node sink
where SourceToSink::flow(source, sink)
select source, "flow to", sink

比如我们随便点一个,比如第一个Source点username,可以看到是定位到了One这个方法的参数部分:

接着点击第一个Sink点sql,是定位到了getStudent这个方法体里的query中的第一个参数,效果还行。

当然,我们继续向下看就可以发现一些误报,比如这里其实限制了传入类型是Long,所以当然不是可控点了:

这里我们可以写一个sanitizer来排除这些情况,主要是判断到传入类型是基础类型、数字类型、泛型数字类型时,就切断数据流:

 predicate isBarrier(DataFlow::Node sanitizer) {
    sanitizer.getType() instanceof PrimitiveType
    or
    sanitizer.getType() instanceof BoxedType
    or
    sanitizer.getType() instanceof NumberType
    or
    exists(ParameterizedType pt |
      sanitizer.getType() = pt and pt.getTypeArgument(0) instanceof NumberType
    )
  }

现在完整代码如下:

import java
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.dataflow.FlowSources

module SourceToSinkConfig implements DataFlow::ConfigSig {
  predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }

  predicate isSink(DataFlow::Node sink) {
    exists(Method method, MethodCall call |
      method.hasName("query") and
      call.getMethod() = method and
      sink.asExpr() = call.getArgument(0)
    )
  }

  predicate isBarrier(DataFlow::Node sanitizer) {
    sanitizer.getType() instanceof PrimitiveType
    or
    sanitizer.getType() instanceof BoxedType
    or
    sanitizer.getType() instanceof NumberType
    or
    exists(ParameterizedType pt |
      sanitizer.getType() = pt and pt.getTypeArgument(0) instanceof NumberType
    )
  }
}

module SourceToSink = TaintTracking::Global<SourceToSinkConfig>;

from DataFlow::Node source, DataFlow::Node sink
where SourceToSink::flow(source, sink)
select source, "flow to", sink

现在的查询结果中就会排除这些点:

参考

SAST工具入门(一)—— Codeql环境搭建与基础语法

Codeql 原理分析(一)

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇