CodeQL初入

一、简介

CodeQL 是一个语义代码分析引擎,它可以扫描发现代码库中的漏洞。使用 CodeQL,可以像对待数据一样查询代码。编写查询条件以查找漏洞的所有变体并处理,同时可以分享个人查询条件。

编写该文章时,主要参考了官方文档 - QL language reference

二、环境搭建

环境搭建整体参考 代码分析引擎 CodeQL 初体验

  • 首先,下载一下CodeQL CLI二进制文件并安装

    1
    2
    3
    4
    5
    6
    7
    # 下载codeql.zip
    wget https://github.com/github/codeql-cli-binaries/releases/latest/download/codeql.zip
    # 解压
    unzip codeql.zip
    # 将codeql添加至path中
    echo "export PATH=\$PATH:/usr/class/codeql" >> ~/.zshrc
    source ~/.zshrc
  • 由于是入门,我们只需要使用初始工作区(starter workspace)就好,因此执行以下命令

    工作区配置参考——Using the starter workspace

    1
    git clone --recursive git@github.com:github/vscode-codeql-starter.git

    注意:该工作区内含了QL库,因此一定要使用递归方式来下拉工作区代码。

    递归方式下拉该仓库后,我们不需要再下拉https://github.com/Semmle/ql这个库了。

    如果觉得下拉很慢,可以挂个代理

    1
    2
    3
    4
    # 设置代理
    git config --global http.proxy <Protocol>://<IP>:<PORT>
    # 取消代理
    git config --global --unset http.proxy
  • 最后,我们还需要在VScode中下载CodeQL的插件——Visual Studio Code Marketplace

    插件下载完成后,还需要在vscode中设置一下Code QL -- Cli: Executable Path为刚刚下载下来的codeql二进制文件执行路径。

  • 上述操作完成后,我们需要先建立一个AST数据库,后续的查询操作等都是在该数据库中完成。

    以C++代码为例,我们可以使用如下命令来建立一个数据库

    1
    codeql database create <database-folder> --language=cpp --command=<prefix command>

    如果省略--command参数,则codeQL会自动检测并使用自己的工具来构建。

    但还是强烈推荐使用自己自定义的参数,尤其是大项目时。

    以构建chrome为例,由于chrome项目过于庞大,因此我们只能针对某个模块来进行分析。

    于是我们可以进行如下操作

    • 先完整编译一个chromium,release不带符号即可。

    • 进入obj目录,将目标模块的obj删除。

    • 执行以下命令,重新编译该模块并构建数据库即可。

      1
      gn gen out/ql && codeql database create <targetFolder> --language=cpp --Command=' ninja -C out/ql chrome'

    建立好的数据库,其目录结构为

    1
    2
    3
    4
    - log\                # 输出的日志信息
    - db-cpp\ # 编译的数据库
    - src.zip # 编译所对应的目标源码
    - codeql-database.yml # 数据库相关配置
  • 之后在VSCode中,

    • 点击“打开工作区”来打开刚刚下拉的vscode-codeql-starter工作区
    • 在CodeQL插件里,打开刚刚生成的database
    • 之后编写自己的CodeQL脚本,并将脚本保存至vscode-codeql-starter\codeql-custom-queries-cpp处,这样import模块时就可以正常引用。
    • 将编写的ql脚本在VSCode中打开,之后点击CodeQL插件中的Run on queue,即可开始查询。
  • 如果想查看某个文件的AST,直接对目标源码,点击右键—CodeQL: View AST即可。第一次执行时会比较慢,稍微等待十分钟左右即可。

CodeQL使用操作参考 - CodeQL分析项目

三、基本语法

基础语法将结合ql代码来讲解。

  • 该QL将输出所有基础块中的空基础块。

    1
    2
    3
    4
    5
    6
    7
    8
    // 首先是引入QL库中的一个包
    import cpp
    // 限定范围在所有的BlockStmt,即所有的基础块
    from BlockStmt b
    // 获取在当前基础块中,语句个数为0的基础块(即空基础块)
    where b.getNumStmt() = 0
    // 输出搜索到的空基础块,与其后面的字符串
    select b, "This is an empty block."
  • 以下是获取某个宏定义位置的ql代码

    1
    2
    3
    4
    5
    6
    import cpp

    from Macro m
    where m.getName() = "_LIBCPP_NO_CFI"
    or m.getName() = "_GLIBC_LIKELY"
    select m,"macro"
  • 该代码获取调用特定函数的代码位置

    1
    2
    3
    4
    5
    import cpp

    from Function f
    where f.getName() = "memcpy"
    select f, "a function named memcpy"

    这并不稀奇,但关键是下一个ql代码

    1
    2
    3
    4
    5
    6
    7
    import cpp

    from FunctionCall call, Function func
    where
    call.getTarget() = func and
    func.getName() = "memcpy"
    select call,"func named memcpy and called"

    FunctionCall将会涵盖所有的函数调用,因此我们可以通过该对象来获取特定函数被调用的位置。

对于所有的类和函数,都可以通过ctrl+右键的形式来查看其源码来了解更多信息。

四、高级语法

1. 谓词

a. 概述

  • 在CodeQL中,函数并不叫“函数”,叫做Predicates(谓词)。为了便于说明,下文中笔者可能会混用函数这个词语,即下文中的 “函数”“谓语” 都是指代同一个内容。

  • 在使用谓词前,我们需要定义一个谓词。谓词的格式如下

    1
    2
    3
    4
    predicate name(type arg)
    {
    statements
    }

    定义谓词有三个步骤

    • 关键词predicate(如果没有返回值),或者结果的类型(如果当前谓词内存在返回值)
    • 谓词的名称
    • 谓词的参数列表
    • 谓词主体

b. 无返回值的谓词

  • 无返回值的谓词以predicate关键词开头。若传入的值满足谓词主体中的逻辑,则该谓词将保留该值。

  • 无返回值谓词的使用范围较小,但仍然在某些情况下扮演了很重要的一个角色,具体功能将在下文中逐渐讲解。

  • 需要注意的是,参数i是一个数据集合

  • 举一个简单的例子

    1
    2
    3
    4
    5
    6
    7
    8
    predicate isSmall(int i) {
    i in [1 .. 9]
    }

    from int i
    where isSmall(i) // 将整数集合i从正无穷大的数据集含,限制至1-9
    select i
    // 输出 1-9的数字

    若传入的i是小于10的正整数,则isSmall(i)将会使得传入的集合i只保留符合条件的值,其他值将会被舍弃。

c. 带返回值的谓词

  • 当我们需要将某些结果从谓词中返回时,与C/C++的return语句不同的是,谓词使用的是一个特殊变量result

  • 举个简单例子

    1
    2
    3
    4
    5
    6
    7
    8
    int getSuccessor(int i) {
    // 若传入的i位于1-9内,则返回i+1
    // 注意这个语法不能用C++语法来理解
    result = i + 1 and i in [1 .. 9]
    }

    select getSuccessor(3) // 输出4
    select getSuccessor(33) // 不输出任何信息

    谓词主体的语法只是为了表述逻辑之间的关系,因此务必不要用一般编程语言的语法来理解。

  • 在谓词主体中,result变量可以像一般变量一样正常使用,唯一不同的是这个变量内的数据将会被返回。

    同时,谓词可能返回多个结果,或者根本不返回任何结果。以下是一个简单的例子。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    string getANeighbor(string country) {
    country = "France" and result = "Belgium"
    or
    country = "France" and result = "Germany"
    or
    country = "Germany" and result = "Austria"
    or
    country = "Germany" and result = "Belgium"
    }

    select getANeighbor("France")
    // 返回两个条目,"Belgium"与"Germany"
  • 谓词不允许描述的数据集合个数不限于有限数量大小的。举个例子

    1
    2
    3
    4
    5
    6
    // 该谓词将使得编译报错
    int multiplyBy4(int i) {
    // i是一个数据集合,此时该集合可能是**无限大小**
    // result集合被设置为i*4,意味着result集合的大小有可能也是**无限大小**
    result = i * 4
    }

    但如果我们仍然需要定义这类函数,则必须限制集合数据大小,同时添加一个bindingset标注。该标注将会声明谓词plusOne所包含的数据集合是有限的,前提是i绑定到有限数量的数据集合。

    1
    2
    3
    4
    5
    6
    7
    8
    bindingset[x] bindingset[y]
    predicate plusOne(int x, int y) {
    x + 1 = y
    }

    from int x, int y
    where y = 42 and plusOne(x, y)
    select x, y

d. 递归

  • 谓词类似于函数,可以递归调用

    同时result变量可以按照任何方式来表达与其他变量之间的关系,因此result变量的赋值不局限于使用=符号。

    以下是一个简单例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    string getANeighbor(string country) {
    country = "France" and result = "Belgium"
    or
    country = "France" and result = "Germany"
    or
    country = "Germany" and result = "Austria"
    or
    country = "Germany" and result = "Belgium"
    or
    country = getANeighbor(result)
    }
    select getANeighbor("Austria")
    // 输出Germany
  • 传递闭包

    谓词的传递闭包是递归谓词,它的结果是通过重复应用原始的谓词来获得的。

    特别要注意的是,原始谓词必须有两个参数(可能包括this或result值),并且这些参数必须具有兼容的类型。

    由于传递闭包是递归的一种常见形式,因此QL有两个有用的缩写,分别是+*

    • 传递闭包(+)

      如果要一次或多次的应用特定谓词,请在谓词后添加一个+符号。

      举个例子,假设定义了一个带有成员谓词getAParent()Person类,其中p.getAParent()会返回p的所有父母。而p.getAParent+()将会返回p的父母、p的父母的父母、等等等等。

      使用+来表示通常会比显式定义递归谓词更简单,p.getAParent+()等价于以下递归谓词:

      1
      2
      3
      4
      5
      Person getAnAncestor() {
      result = this.getAParent()
      or
      result = this.getAParent().getAnAncestor()
      }
    • 自反传递闭包(*)

      这个类似于上面的传递闭包。与之前所不同的是,使用*可以让谓词调用自己一次至多次。

      例如:p.getAParent*()将会输出p的祖先,或者p。该谓词调用等价于以下谓词:

      1
      2
      3
      4
      5
      Person getAnAncestor2() {
      result = this
      or
      result = this.getAParent().getAnAncestor2()
      }

参考:Predicates - QL language reference

2. 类

  • 以上面ql中的各种类为例(例如Function类),这些类的设计将特定一类的代码归结为一处,以便于后续查询的使用。而如果我们需要自定义特定的类,那该怎么做呢?

    CodeQL中的类,并不意味着建立一个新的对象,而只是表示特定一类的数据集合,请注意区分。

  • 定义一个类,需要三个步骤

    • 使用关键字class

    • 起一个类名,其中类名必须是首字母大写的。

    • 确定是从哪个类中派生出来的

      使用的基类,除了cpp包中定义的各种类以外,还包括基本类型,即booleanfloatintstring以及date

    • 类的主体

  • 以下是一个简单的例子,这个例子是官方的一个样例。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class OneTwoThree extends int {
    OneTwoThree() { // characteristic predicate
    this = 1 or this = 2 or this = 3
    }

    string getAString() { // member predicate
    result = "One, two or three: " + this.toString()
    }

    predicate isEven() { // member predicate
    this in [1 .. 2] //
    }
    }

    from OneTwoThree i
    where i = 1 or i.getAString() = "One, two or three: 2"
    select i
    // 输出1和2
    • 特征谓词类似于C++中的类构造函数,它将会进一步限制当前类所表示数据的集合。例如上面的特征谓词

      1
      2
      3
      OneTwoThree() { // characteristic predicate
      this = 1 or this = 2 or this = 3
      }

      它将数据集合从原先的Int集,进一步限制至1-3这个范围。

      this变量表示的是当前类中所包含的数据集合。与result变量类似,this同样是用于表示数据集合直接的关系。

    • 在特征谓词中,比较常用的一个关键字是exists。该关键字的语法如下

      1
      2
      3
      4
      exists(<variable declarations> | <formula>)
      // 以下两个exists所表达的意思等价。
      exists(<variable declarations> | <formula 1> | <formula 2>
      exists(<variable declarations> | <formula 1> and <formula 2>

      这个关键字的使用引入了一些新的变量。如果变量中至少有一组值可以使formula成立,那么该值将被保留。

      一个简单的例子

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      import cpp

      class NetworkByteSwap extends Expr{
      NetworkByteSwap()
      {
      // 对于MacroInvocation这个大类的数据集合来说,
      exists(MacroInvocation mi |
      // 如果存在宏调用,其宏名称满足特定正则表达式
      mi.getMacroName().regexpMatch("ntoh(s|l|ll)") and
      // 将这类数据保存至当前类中
      this = mi.getExpr()
      )
      }
      }

      from NetworkByteSwap n
      select n, "Network byte swap"
    • 与之对应的还有成员谓词,如下例所示

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      class OneTwoThree extends int {
      OneTwoThree() { // characteristic predicate
      this = 1 or this = 2 or this = 3
      }

      string getAString() { // member predicate
      result = "One, two or three: " + this.toString()
      }

      predicate isEven() { // member predicate
      this in [1 .. 2]
      }
      }
      select 1.(OneTwoThree).getAString() // 输出"One, two or three: 1"
      //select 4.(OneTwoThree).getAString() // 无输出

      其中,1.(OneTwoThree).getAString()会将int类型的1转换为OneTwoThree类型。在转换的过程中会丢弃不满足OneTwoThree类中限定条件的数据。因此4.(OneTwoThree).getAString()将不会输出任何信息,因为整数4在转换的过程中被丢弃了。

  • 与C++类似,CodeQL中类里可以声明一个类字段,如下例所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class SmallInt extends int {
    SmallInt() { this = [1 .. 10] }
    }

    class DivisibleInt extends SmallInt {
    SmallInt divisor; // declaration of the field `divisor`
    DivisibleInt() { this % divisor = 0 }

    SmallInt getADivisor() { result = divisor }
    }

    from DivisibleInt i
    select i, i.getADivisor()
  • 需要注意的是,

    • 每个类都不能继承自己

    • 不能继承final类

    • 不能继承不相容的类

      这一点需要额外说明一下,从某个基类派生出的类,将拥有基类的所有数据集合范围。如果某个类继承了多个基类,那么该类内含的数据集合,将是两个基类数据集合的交集

参考:class - QL language reference

六、数据流分析与污点追踪

该部分内容主要 参考 翻译自:Analyzing data flow in C and C++ - CodeQL documentation

参考了About data flow analysis的部分内容

  • 我们可以在CodeQL中,使用数据流分析来跟踪可能导致漏洞的潜在恶意数据流。
  • 数据流分析可以分析出变量在程序中各节点上可能的值,并确定这些值如何在程序中传输以及使用方式。
  • 数据流分为两个部分:局部数据流以及全局数据流

1. 局部数据流

局部数据流指的是在一个单独函数内的数据流。局部数据流比全局数据流分析的更加简单、迅速,同时也更加精确。

a. 使用局部数据流

  • 局部数据流的库函数主要位于DataFlow模块中。该模块定义了一个类Class,这个类用于表示数据可以流经的任何元素。

  • Node类分为两种,分别是表达式节点ExprNode与参数节点ParameterNode。我们可以使用谓词asExprasParameter,将数据流结点与表达式节点/参数结点之间进行映射。

    注意:参数结点ParameterNode指的是当前函数参数的数据流结点。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Node {
    /** Gets the expression corresponding to this node, if any. */
    Expr asExpr() { ... }

    /** Gets the parameter corresponding to this node, if any. */
    Parameter asParameter() { ... }

    ...
    }

    或者使用谓词exprNode以及parameterNode

    1
    2
    3
    4
    5
    6
    7
    8
    9
    /**
    * Gets the node corresponding to expression `e`.
    */
    ExprNode exprNode(Expr e) { ... }

    /**
    * Gets the node corresponding to the value of parameter `p` at function entry.
    */
    ParameterNode parameterNode(Parameter p) { ... }
  • 谓词localFlowStep(Node nodeFrom, Node nodeTo)可以分析出从nodeFromnodeTo中的元素之间数据流动的方式。该谓词可以通过使用符号+*来进行递归调用,或者使用预定义好的递归谓词localFlow

    以下是一个用于查找从参数source到表达式sink的例子

    1
    DataFlow::localFlow(DataFlow::parameterNode(source), DataFlow::exprNode(sink))

b. 使用局部污点追踪

  • 局部污点追踪通过包括非保留值的流程步骤来扩展了局部数据流,例如以下C++代码

    1
    2
    int i = tainted_user_input();
    some_big_struct *array = malloc(i * sizeof(some_big_struct));

    由于输出的变量i被污染,因此使用变量imalloc函数参数也被污染。

  • 局部污点追踪的库函数主要位于TaintTracking模块中。与局部数据流分析类似,污点追踪同样有谓词localTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo)用于污点分析,同样有递归版本的localTaint谓词。

    一个简单的例子,查找从参数source到表达式sink的污点传播。

    1
    TaintTracking::localTaint(DataFlow::parameterNode(source), DataFlow::exprNode(sink))

c. 例子

  • 这个例子是用于查找传入fopen函数的文件名称

    1
    2
    3
    4
    5
    6
    import cpp

    from Function fopen, FunctionCall fc
    where fopen.hasQualifiedName("fopen")
    and fc.getTarget() = fopen
    select fc.getArgument(0)

    但上面的ql代码只会将文件名参数的表达式输出,而这并不是可能传递给它的值。因此我们需要使用局部数据流分析来找到所有可流入该参数的表达式。

    1
    2
    3
    4
    5
    6
    7
    import semmle.code.cpp.dataflow.DataFlow

    from Function fopen, FunctionCall fc, Expr src
    where fopen.hasQualifiedName("fopen")
    and fc.getTarget() = fopen
    and DataFlow::localFlow(DataFlow::exprNode(src), DataFlow::exprNode(fc.getArgument(0)))
    select src

    这样它将会输出可能流入fopen文件名参数的所有变量的表达式

    现在我们可以稍微将source改一下,将exprNode改成parameterNode,这样就可以查询出既是当前函数的参数,又可以作为fopen的文件名参数的表达式。

    1
    2
    3
    4
    5
    6
    7
    import semmle.code.cpp.dataflow.DataFlow

    from Function fopen, FunctionCall fc, Parameter p
    where fopen.hasQualifiedName("fopen")
    and fc.getTarget() = fopen
    and DataFlow::localFlow(DataFlow::parameterNode(p), DataFlow::exprNode(fc.getArgument(0)))
    select p
  • 以下这个例子将会查找格式字符串中没有被硬编码的格式化函数的调用。

    格式化函数包括但不限于各种printf函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import semmle.code.cpp.dataflow.DataFlow
    import semmle.code.cpp.commons.Printf

    from FormattingFunction format, FunctionCall call, Expr formatString
    where call.getTarget() = format
    and call.getArgument(format.getFormatParameterIndex()) = formatString
    and not exists(DataFlow::Node source, DataFlow::Node sink |
    DataFlow::localFlow(source, sink) and
    source.asExpr() instanceof StringLiteral and
    sink.asExpr() = formatString
    )
    select call, "Argument to " + format.getQualifiedName() + " isn't hard-coded."

2. 全局数据流

全局数据流跟踪整个程序的数据流,因此比局部数据流更强大。但全局数据流的准确性不如本地数据流,并且通常需要更多的时间和内存来执行分析。

a. 使用全局数据流

通过继承DataFlow::Configuration类来使用全局数据流库。

1
2
3
4
5
6
7
8
9
10
11
class MyDataFlowConfiguration extends DataFlow::Configuration {
MyDataFlowConfiguration() { this = "MyDataFlowConfiguration" }

override predicate isSource(DataFlow::Node source) {
...
}

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

DataFlow::Configuration类中定义了如下几个谓词:

  • isSource定义数据可能从何处流出
  • isSink定义数据可能流向的位置
  • isBarrier: 可选,限制数据流
  • isBarrierGuard: 可选,限制数据流
  • isAdditionalFlowStep: 可选,添加其他流程步骤

在特征谓词MyDataFlowConfiguration()中定义了当前Configuration的名称,因此内部的"MyDataFlowConfiguration"需要替换成自己的名称。

使用谓词hasFlow(DataFlow::Node source, DataFlow::Node sink)来执行全局数据流分析

1
2
3
from MyDataFlowConfiguration dataflow, DataFlow::Node source, DataFlow::Node sink
where dataflow.hasFlow(source, sink)
select source, "Data flow to $@.", sink, sink.toString()

b. 使用全局污点追踪

与局部污点追踪类似,全局污点追踪针对的是全局数据流。全局污点追踪通过其他不保留值的步骤来扩展了全局数据流。

通过继承TaintTracking::Configuration类以使用全局污点追踪的库函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
import semmle.code.cpp.dataflow.TaintTracking

class MyTaintTrackingConfiguration extends TaintTracking::Configuration {
MyTaintTrackingConfiguration() { this = "MyTaintTrackingConfiguration" }

override predicate isSource(DataFlow::Node source) {
...
}

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

在配置中定义了以下谓词:

  • isSource:定义污点可能从何处流出
  • isSink:定义污点可能流入的地方
  • isSanitizer:可选,限制污点流
  • isSanitizerGuard:可选,限制污点流
  • isAdditionalTaintStep:可选,添加其他污染步骤

使用谓词hasFlow(DataFlow::Node source, DataFlow::Node sink)以执行污点追踪分析。

c. 例子

  • 以下数据流分析用于追踪从环境变量到打开文件的数据流

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    import semmle.code.cpp.dataflow.DataFlow

    class EnvironmentToFileConfiguration extends DataFlow::Configuration {
    EnvironmentToFileConfiguration() { this = "EnvironmentToFileConfiguration" }

    override predicate isSource(DataFlow::Node source) {
    exists (Function getenv |
    source.asExpr().(FunctionCall).getTarget() = getenv and
    getenv.hasQualifiedName("getenv")
    )
    }

    override predicate isSink(DataFlow::Node sink) {
    exists (FunctionCall fc |
    sink.asExpr() = fc.getArgument(0) and
    fc.getTarget().hasQualifiedName("fopen")
    )
    }
    }

    from Expr getenv, Expr fopen, EnvironmentToFileConfiguration config
    where config.hasFlow(DataFlow::exprNode(getenv), DataFlow::exprNode(fopen))
    select fopen, "This 'fopen' uses data from $@.",
    getenv, "call to 'getenv'"
  • 以下污点追踪代码用于追踪从调用ntohl到操作数组索引的数据流。该代码使用Guards库以识别经过边界检查的表达式,同时还定义了谓词isSanitizer以避免污点分析经过特定数据,最后定义了isAdditionalTaintStep用于将流从边界循环添加至循环索引。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    import cpp
    import semmle.code.cpp.controlflow.Guards
    import semmle.code.cpp.dataflow.TaintTracking

    class NetworkToBufferSizeConfiguration extends TaintTracking::Configuration {
    NetworkToBufferSizeConfiguration() { this = "NetworkToBufferSizeConfiguration" }

    override predicate isSource(DataFlow::Node node) {
    node.asExpr().(FunctionCall).getTarget().hasGlobalName("ntohl")
    }

    override predicate isSink(DataFlow::Node node) {
    exists(ArrayExpr ae | node.asExpr() = ae.getArrayOffset())
    }

    override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
    exists(Loop loop, LoopCounter lc |
    loop = lc.getALoop() and
    loop.getControllingExpr().(RelationalOperation).getGreaterOperand() = pred.asExpr() |
    succ.asExpr() = lc.getVariableAccessInLoop(loop)
    )
    }

    override predicate isSanitizer(DataFlow::Node node) {
    exists(GuardCondition gc, Variable v |
    gc.getAChild*() = v.getAnAccess() and
    node.asExpr() = v.getAnAccess() and
    gc.controls(node.asExpr().getBasicBlock(), _)
    )
    }
    }

    from DataFlow::Node ntohl, DataFlow::Node offset, NetworkToBufferSizeConfiguration conf
    where conf.hasFlow(ntohl, offset)
    select offset, "This array offset may be influenced by $@.", ntohl,
    "converted data from the network"

七、CodeQL U-Boot Challenge

  • 纸上得来终觉浅,绝知此事要躬行。简单翻阅QL文档是学不到什么的,我们需要自己动手实践一下。

    下面笔者将讲述github learning lab中,用于学习CodeQL的一个入门课程 - CodeQL U-Boot Challenge (C/C++)

  • Step1: 了解从何处获取帮助

  • Step2: 设置IDE

    • 下载VSCode以及CodeQL插件,还有CodeQL CLI文件。
    • 下载CodeQL starter工作区
    • 下载U-Boot CodeQL database并解压
    • 克隆当前github课程仓库
    • 将当前课程仓库的文件夹添加至之前下载的VScode starter工作区,同时将之前下载的U-Boot数据库导入至VScode
    • 一切就绪!
  • Step3: 编写一个简单的查询。在这里我们用于查询strlen函数的定义位置。

    1
    2
    3
    4
    5
    import cpp

    from Function f
    where f.getName() = "strlen"
    select f, "a function named strlen"
  • Step4: 分析这个简单的查询,之后查询一下memcpy函数

    1
    2
    3
    4
    5
    import cpp

    from Function f
    where f.getName() = "memcpy"
    select f, "a function named memcpy"
  • Step5: 使用不同的类以及不同的谓语。这里我们编写QL查找名为ntohsntohl以及ntohll的宏定义。

    1
    2
    3
    4
    5
    6
    import cpp 

    from Macro macro
    //where macro.getName() = "ntohs" or macro.getName() = "ntohl" or macro.getName() = "ntohll"
    where macro.getName().regexpMatch("ntoh(s|l|ll)")
    select macro
  • Step6: 使用双变量。通过使用多个变量来描述复杂的代码关系,查询特定函数的调用位置。

    1
    2
    3
    4
    5
    import cpp

    from FunctionCall c, Function f
    where c.getTarget() = f and f.getName() == "memcpy"
    select c
  • Step7: 使用Step6的技巧,查询宏定义的调用位置。

    1
    2
    3
    4
    5
    import cpp

    from MacroInvocation invoc
    where invoc.getMacroName().regexpMatch("ntoh(s|l|ll)")
    select invoc
  • Step8: 改变select的输出。查找这些宏调用所扩展到的顶级表达式。

    1
    2
    3
    4
    5
    import cpp

    from MacroInvocation mi
    where mi.getMacro().getName().regexpMatch("ntoh(s|l|ll)")
    select mi.getExpr() // 注意这里的.getExpr()
  • Step9:编写一个类。用exists关键字来引入一个临时变量,以设置当前类的数据集合;特征谓词在声明时会被调用以确定当前类的范围,类似于C++构造函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import cpp

    class NetworkByteSwap extends Expr{
    NetworkByteSwap()
    {
    exists(MacroInvocation mi |
    mi.getMacroName().regexpMatch("ntoh(s|l|ll)") and
    this = mi.getExpr()
    )
    }
    }

    from NetworkByteSwap n
    select n, "Network byte swap"
  • Step10:数据流分析。若memcpylength直接来自于远程,而不加以验证,那么这将会产生OOB漏洞。以下编写的CodeQL查询针对的就是这类情况,它将使用全局数据流分析技术,查出真正的CVE漏洞。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    import cpp
    import semmle.code.cpp.dataflow.TaintTracking
    import DataFlow::PathGraph

    // 设置用于交换网络数据的类
    class NetworkByteSwap extends Expr{
    NetworkByteSwap() {
    exists(MacroInvocation mi |
    mi.getMacroName().regexpMatch("ntoh(s|l|ll)") and
    this = mi.getExpr()
    )
    }
    }
    // 设置污点跟踪的分析信息
    class Config extends TaintTracking::Configuration{
    Config() { this = "NetworkToMemFuncLength"}
    // 覆盖原先的isSource. 该谓语用于表示满足控制流源头的表达式.
    override predicate isSource(DataFlow::Node source){
    source.asExpr() instanceof NetworkByteSwap
    }
    // 覆盖原先的isSink, 该谓语用于表示满足控制流尽头的表达式.
    override predicate isSink(DataFlow::Node sink){
    exists(FunctionCall c | c.getTarget().getName() = "memcpy" and sink.asExpr() = c.getArgument(2))
    }
    }
    // 查询
    from Config cfg, DataFlow::PathNode source, DataFlow::PathNode sink
    where cfg.hasFlowPath(source, sink)
    select sink, source, sink, "Network byte swap flows to mmcpy"

八、结语

  • 当我们深入学习Codeql之后,我们就可以使用CodeQL挖掘特定漏洞模式的漏洞。
  • CodeQL入门大致如上所示,更深层次的使用需要翻阅各种QL API来结合使用,CodeQL library for C and C++ 由此进。
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2020-2021 Kiprey
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~