前言

这是一个来自github learning lab的项目:

CodeQL U-Boot Challenge (C/C++)

Step1:Welcome

本课程是为了帮助您快速学习 CodeQL,我们的查询语言和代码分析引擎。 目标是使用 CodeQL 及其库分析 C/C++ 代码,在称为 U-Boot 的开源软件中找到几个远程代码执行 (RCE) 漏洞。 要找到真正的漏洞,您需要编写一系列查询,使它们在课程的每一步都更加精确

Step2:安装环境

CodeQL的安装无需多言,

在此处下载U-Boot的代码数据库:

U-Boot download

然后将其加入工作区即可

Step3:第一条查询语句

这里我们要完成第一个查询语句:找到名称为strlen的函数

编辑3_function_definitions.ql,代码如下:

1
2
3
4
5
import cpp

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

这样就能找到strlen函数的定义点了

然后就是将其提交到step-3分支:

1
2
3
4
5
6
git checkout main
git pull
git checkout -b step-3
git add .
git commit -a -m "First Query"
git push -u origin step-3

或者提交到main分支也可以:

1
2
3
git add .
git commit -m "Any message here - why not step 3"
git push origin main

然后等待check完成即可

Step4:查询解析

这一步骤就是对前面查询语句的解析

除此之外,需要完成对memcpy函数的查询

4_memcpy_definitions.ql如下:

1
2
3
4
import cpp
from Function f
where f.getName() = "memcpy"
select f, "a function named memcpy"

然后提交

Step5:使用不同的类及其谓词

我们想要识别从网络数据提供的整数值。 发现这些的一个好方法是寻找网络排序转换宏的使用,例如 ntohl、ntohll 和 ntohs

在查询的 from 部分中,您声明了一些变量,并说明了这些变量的类型。 类型告诉我们变量的可能值是什么

在前面的查询中,您正在查询类 Function 中的值以在源代码中查找函数。 我们必须查询不同的类型才能在源代码中找到宏

编辑文件 5_macro_definitions.ql 编写一个查询,查找名为 ntohs、ntohl 或 ntohll 的宏的定义 可以使用 or 关键字来组合您希望至少满足一个条件的多个条件。 在这里,我们对三个可能的宏名称感兴趣。

代码如下:

1
2
3
4
5
import cpp

from Macro m
where m.getName() = "ntohs" or m.getName() = "ntohl" or m.getName() = "ntohll"
select m, "nto(hs|hl|hll) Macro"

也可以:

1
2
3
4
5
import cpp

from Macro m
where m.getName().regexpMatch("ntoh(s|l|ll)")
select m

Step6:关联两个变量

在第 4 步中,您编写了一个查询,用于查找代码库中名为 memcpy 的函数的定义。 现在,我们想在代码库中找到所有对 memcpy 的调用

一种方法是声明两个变量:一个表示函数,一个表示函数调用。 然后,您必须在 where 部分中创建这些变量之间的关系,以便它们仅限于名为 memcpy 的函数,并准确调用这些函数

这一步骤的关键其实是要找到正在调用的目标函数的谓词

可以参考以下示例:

1
2
3
4
5
6
7
8
9
10
//Finds calls to std::map<...>::find()
import cpp

from FunctionCall call, Function fcn
where
call.getTarget() = fcn and
fcn.getDeclaringType().getSimpleName() = "map" and
fcn.getDeclaringType().getNamespace().getName() = "std" and
fcn.hasName("find")
select call

编辑6_memcpy_calls.ql

1
2
3
4
5
import cpp

from Function f, FunctionCall call
where f.getName() = "memcpy" and call.getTarget() = f
select call, "a call to memcpy"

我们可以把其简化为:

1
2
3
4
5
import cpp

from FunctionCall call
where call.getTarget().getName() = "memcpy"
select call, "a call to memcpy"

Step7:关联两个变量

在第 5 步中,您编写了一个查询,用于查找代码库中名为 ntohs、ntohl 和 ntohll 的宏的定义。 现在,我们想在代码库中找到这些宏的所有调用

这将类似于您在第 6 步中所做的,在该步骤中您为函数和函数调用创建变量,并限制它们查找特定函数及其调用

注意:宏调用是源代码中调用特定宏的位置。 这类似于函数调用是源代码中调用特定函数的位置

编辑7_macro_invocations.ql

1
2
3
4
5
import cpp

from MacroInvocation mi
where mi.getMacro().getName().regexpMatch("ntoh(s|l|ll)")
select mi, "Macro used"

Step8:更改选定的输出

在上一步中,您找到了我们感兴趣的宏的调用。修改您的查询以找到这些宏调用扩展为的顶级表达式

注意:表达式是可以在运行时具有值的源代码元素。调用宏可以将各种源代码元素带入范围,包括表达式

使用上一个查询编辑文件8_macro_expressions.ql 使用选择部分中的getExpr()谓词返回所需的表达式

如下:

1
2
3
4
5
import cpp

from MacroInvocation mi
where mi.getMacro().getName().regexpMatch("ntoh(s|l|ll)")
select mi.getExpr()

Step9:编写自己的类

在这一步中,我们将学习如何编写自己的 CodeQL 类。 这将帮助我们使查询的逻辑更具可读性、更易于重用和更易于细化

我们希望找到与上一步相同的结果,即对应于 ntohl、ntohs 和 ntohll 宏调用的顶级表达式。 如果我们可以直接引用所有这样的表达式将会很有用,就像我们可以使用标准库中的MacroInvocation来引用所有宏调用一样

我们将定义一个类来准确描述这组表达式,并在本课程的最后一步中使用它

Expr 类是所有表达式的集合,我们对更具体的表达式集感兴趣,所以我们编写的类将是 Expr 的子类

到目前为止,我们已经在查询子句的 from 部分声明了变量。 有时我们在查询的其他部分需要临时变量,并且不想在查询子句中暴露它们。 exists 关键字可以帮助我们做到这一点。 它是一个量词:它引入临时变量并检查它们是否满足特定条件

例如:

1
2
3
from Person t
where exists(string c | t.getHairColor() = c)
select t

此查询选择所有头发颜色为字符串的人。 所以我们会得到所有不秃头的人,因为我们能够找到一个定义他们头发颜色的 c。 除了知道它存在之外,我们在查询中实际上并不需要 c

我们需要编写自己的NetworkByteSwap

使用以下模板编辑文件 9_class_network_byteswap.ql

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

class NetworkByteSwap extends Expr {
NetworkByteSwap () {
// TODO: replace <class> and <var>
exists(<class> <var> |
// TODO: <condition>
)
}
}

from NetworkByteSwap n
select n, "Network byte swap"

这个类扩展了 Expr,这意味着它是 Expr 的一个子类,它首先从 Expr 中获取所有值。 现在您需要将其限制为仅我们感兴趣的表达式,这些表达式满足步骤 8 的条件

您可以通过编辑特征谓词 NetworkByteSwap() { … } 来做到这一点。 该模板包括存在量词,这将有所帮助

在 exists 中声明一个引用宏调用的临时变量

将此宏调用约束在存在的条件部分。在步骤 8 中使用查询的 where 部分中的相同逻辑

ql代码如下:

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

class NetworkByteSwap extends Expr {
NetworkByteSwap () {
// TODO: replace <class> and <var>
exists(MacroInvocation mi |
// TODO: <condition>
mi.getMacro().getName().regexpMatch("ntoh(s|l|ll)") and
mi.getExpr() = this
)
}
}

from NetworkByteSwap n
select n, "Network byte swap"

Step10:数据流和污点跟踪分析

在第 9 步中,我们在源代码中发现了可能具有从远程输入提供的整数的表达式,因为它们正在通过调用 ntoh、ntohll 或 ntohs 进行处理。这些可以被认为是远程输入的来源

在第 6 步中,我们找到了对 memcpy 的调用。当它们的长度参数由远程用户控制时,这些调用可能是不安全的。它们的长度参数可以被认为是接收器:它们不应该在没有进一步验证的情况下接收用户控制的值

结合这些信息,我们知道,如果受污染的数据在 memcpy 调用的长度参数中从网络整数源流向接收器,则代码很容易受到攻击

但是,我们如何知道来自特定源的数据是否可能到达特定接收器?这称为数据流或污点跟踪分析。鉴于结果的数量(数百个 memcpy 调用和大量宏调用),手动对所有这些情况进行分类将是相当多的工作

为了使我们的分类工作更容易,我们将让 CodeQL 为我们做这个分析

您现在将编写一个查询来跟踪从网络控制的整数到 memcpy 长度参数的污染数据流。结果,您会发现 9 个真正的漏洞!

为此,我们将使用 CodeQL 污点跟踪库。该库允许您描述源和接收器,当来自给定源的受污染数据流向接收器时,它的谓词hasFlowPath成立

使用下面的模板编辑文件 10_taint_tracking.ql

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
/**
* @kind path-problem
*/

import cpp
import semmle.code.cpp.dataflow.TaintTracking
import DataFlow::PathGraph

class NetworkByteSwap extends Expr {
// TODO: copy from previous step
}

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

override predicate isSource(DataFlow::Node source) {
// TODO
}
override predicate isSink(DataFlow::Node sink) {
// TODO
}
}

from Config cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where cfg.hasFlowPath(source, sink)
select sink, source, sink, "Network byte swap flows to memcpy"
  • 从第 9 步复制并粘贴您对 NetworkByteSwap 类的定义
  • 编写 isSource 谓词。这应该识别调用 ntohl、ntohs 或 ntohll 中的表达式
    • 您已经在第 9 步的 NetworkByteSwap 类中描述了这些表达式。这里我们需要检查源是否对应于属于该类的值
    • 要检查一个值是否属于 CodeQL 类,请使用 instanceof 构造
    • 请注意,源变量是 DataFlow::Node 类型,而您的 NetworkByteSwap 类是 Expr 的子类,因此我们不能只编写 NetworkByteSwap 的源实例。 (试试这个,编译器会给你一个错误。)在源代码上使用自动完成来发现让我们将其视为 Expr 的谓词
  • 编写 isSink 谓词: sink 应该是调用 memcpy 的 size 参数 使用自动完成查找返回函数调用的第 n 个参数的谓词 使用您在编写 isSource 时发现的谓词将接收器视为 Expr

运行您的查询。请注意,第一次运行将比之前的查询花费一点时间,因为数据流分析更复杂

代码如下:

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
37
/**
* @kind path-problem
*/

import cpp
import semmle.code.cpp.dataflow.TaintTracking
import DataFlow::PathGraph

class NetworkByteSwap extends Expr {
// TODO: copy from previous step
NetworkByteSwap(){
exists( MacroInvocation mi |
mi.getMacro().getName().regexpMatch("ntoh(s|l|ll)") and
this = mi.getExpr()
)
}
}

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

override predicate isSource(DataFlow::Node source) {
// TODO
source.asExpr() instanceof NetworkByteSwap
}
override predicate isSink(DataFlow::Node sink) {
// TODO
exists( FunctionCall fc |
fc.getTarget().getName() = "memcpy" and
sink.asExpr() = fc.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 memcpy"

可以看到确实找出了如下的可疑点:

image.png