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

 

背景

需要在U-Boot中寻找一组9个远程代码执行漏洞
漏洞点位于memcpy函数
但并非所有调用memcpy函数的都存在漏洞
所以我们需要减少误报率,找到真正存在漏洞的memcpy调用

放上题目链接

关于环境搭建根据题目提示就可以顺利完成哦
也可以参考我的文章”CodeQL for VSCode搭建流程”
不出意外会放在我的博客中

 

Step 3 – our first query

在项目中寻找所有名为’strlen’的函数
语法类似于sql语句
import cpp: 导入c++规则库
From Function f1: 声明一个Function类的变量为f1
where f1.getName() = "strlen": Function.getName()顾名思义用于获取此声明的名称,也就是名称和”strlen”相等的声明会被挑选出来
select f1,"a function named strlen": select后接要在result中展示的项目,用逗号分隔
3_function_definitions.ql

import cpp

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

直接在main提交

commit中查看结果,通过

 

Step 4 – Anatomy of a query

仿照上一步,在项目中寻找所有名为’memcpy’的函数
4_function_definitions.ql

import cpp

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

提交查看结果,通过

 

Step 5 – Using different classes and their predicates

自定义规则,查找三个名为ntohs, ntohl or ntohll的宏定义
需要一个紧凑的查询,而不是三个查找案例组合在一起
给出以下两种方法

  1. 利用正则表达式
    string类有一个方法regexpMatch,接收器将参数与正则表达式匹配
    那我们需要先找到宏定义,再对该字符串进行正则匹配(使用的java的匹配模式)
    5_function_definitions.ql
import cpp

from Macro m
where m.getName().regexpMatch("ntoh(s|l|ll)")
select m,"macros named ntohs, ntohl or ntohll"

运行

  1. 使用集合表达式
    给出的格式:<your_variable_name> in [“bar”, “baz”, “quux”]
import cpp

from Macro m
where m.getName() in ["ntohs","ntohl","ntohll"]
select m,"macros named ntohs, ntohl or ntohll"

运行后和之前的结果相同,提交通过

PS:
上学的时候为了过考试自学的c++,就是一些简单的语法
看题目说明也没看明白ntoh 族函数到底是个啥
后来看见了swing的文章
才知道ntoh族函数通常用来进行网络字节序到主机字节序的转换
其实自己看到的时候就应该去查的,但是因为对题目影响不大就犯懒没去:-(
以后不能这样了!看见没见过的看不懂的一定要去弄清楚

 

Step 6 – Relating two variables

找到所有对memcpy函数的调用
先看看给的例子
FunctionCall.getTarget()查询该函数被调用的位置
直接和Function类型的fcn对比值,说明他返回的值应该就是Function类型(这点在下面优化中会用到)

通过Function.hasName()获取方法名

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

如果你想要省略中间变量Function,使查询的更加紧凑,可以参考以下两个对比
c1.getClass2()返回的是Class2类型的值,因此可以直接调用Class2的方法

from Class1 c1, Class2 c2
where
  c1.getClass2() = c2 and
  c2.getProp() = "something"
select c1


from Class1 c1
where c1.getClass2().getProp() = "something"
select c1

根据以上案例思考
我们需要找到memcpy函数被调用的位置,可以使用
FunctionCall.getTarget()
并希望查询更加紧凑,可以直接获取找到的函数的名称并进行判断
FunctionCall.getTarget().getName="memcpy"

6_memcpy_calls.ql

import cpp
from FunctionCall functioncall
where functioncall.getTarget().hasName("memcpy")
select functioncall

提交通过

 

Step 7 – Relating two variables, continued

寻找所有对ntoh*宏定义的调用

这里用到的是MacroInvocation这个类,顾名思义就是宏定义调用的类
鼠标悬浮看其注释也能看出来

那么我们就可以通过getMacro()寻找被调用的宏定义,并得到返回的Macro类型值
再获得找到的Macro名称进行正则匹配,即可获得我们想要的结果

import cpp
 from MacroInvocation macInvo
 where macInvo.getMacro().getName().regexpMatch("ntoh.*")
 select macInvo

(备注:关于正则表达式,不太会写,找的java正则api看的。
.表示匹配除换行符 \n 之外的任何单字符,*表示零次或多次,
我这里希望得到的结果是以ntoh开头的宏定义都会被选中。
如果有不对的地方,还希望可以被提出指正◔ ‸◔)

提交通过

 

Step 8 – Changing the selected output

根据提示,使用getExpr()这个predicate
先看看这个getExpr()的注释说明
是用来获取宏定义表达式的
如果顶级拓展元素不是表达式,它只是一条语句,将不会被选中列为结果

使用select macInvo.getExpr(),就能获得宏定义调用相关的表达式
8_macro_expressions.ql

import cpp
 from MacroInvocation macInvo
 where macInvo.getMacro().getName().regexpMatch("ntoh.*")
 select macInvo.getExpr()

例如点击其中一个结果,就会跳转至下图位置

提交通过

那么查询表达式和查询调用的区别是啥?
看注释说明,
getExpr()

Gets a top-level expression associated with this macro invocation,if any.
Note that this predicate will fail if the top-level expanded element is not an expression (for example if it is a statement).
This macro is intended to be used with macros that expand to a complete expression.
In other cases, it may have multiple results or no results.

获取关于宏调用的顶级表达式
注意,如果顶级扩展元素不是一个表达式的话查询将失败(例如,它是一个语句)
此宏用于扩展为完整表达式的宏,在其他情况下可能会有多个结果或没有结果

getMacro()

Gets the macro that is being accessed.
获取正在访问的宏

getMacro()会获取所有调用的宏,即使他只是一个语句
getExpr()只会获取宏调用的顶级表达式
所以getExpr()得到的结果集应该包含于getMacro()的结果集
这里放上语句和表达式的区别讨论链接

 

Step 9 – Write your own class

首先看看学习exists关键词给出的例子:
这个规则只是为了获取不秃头的所有人

不秃头的人都会有头发,那么他们的头发都会对应一个或多个颜色
其中t.getHairColor()会返回一个string类型的值,例如”red”
如果我们需要获得不秃头的人,我们并不需要知道他们头发的具体颜色,只需要知道t.getHairColor()会返回string类型的值即可,因为秃头getHairColor()时,不会返回任何值

所以我们利用string类型的变量完成该操作
更好的方式是使用exists关键词,因为我们只是在where中使用该变量
例如,exists(string c | t.getHairColor() = c)使用了string类型的临时变量,用于获取t.getHairColor()返回了string值的t,也就是查询了所有头发颜色的值为string类型的人

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

/*在CodeQL中,以下代码功能同于以上代码,给出只是为了更好地理解*/
from Person t, string c
where t.getHairColor() = c
select t

再来看看类定义中给出的案例

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 = 2
  }
}

以上代码定义了一个名为OneTwoThree的类,继承于int
类似于构造函数的部分是this = 1 or this = 2 or this = 3
文档中解释说明这个类中包括了1,2,3这三个值
运行以下规则,可以发现ott中确实有1,2,3这三个值

import cpp
 /*from MacroInvocation macInvo
 where macInvo.getMacro().getName().regexpMatch("ntoh.*")
 select macInvo.getExpr()*/
 class OneTwoThree extends int {
    OneTwoThree() { // characteristic predicate
      this = 1 or this = 2 or 3=this
    }

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

    predicate isEven() { // member predicate
      this = 2
    }
  }

  from OneTwoThree ott
  select ott

其中还有一个熟悉的单词predicate
这个是在类的主体内定义的谓词,是使用变量来限制类中可能的值的逻辑属性
举个例子,运行以下规则,就会得到值2

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

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

    predicate isEven() { // member predicate
      this = 2
    }
  }

  from OneTwoThree ott
  where ott.isEven()
  select ott

运行截图:

再更改规则如下:

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

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

    predicate isEven() { // member predicate
      this = 2
    }
  }

  from OneTwoThree ott
  where ott = 2
  select ott

他们会得到相同的结果

也就是说where ott.isEven()where ott = 2做出的是相同的限制
那么我们也就能更好地理解,predicate特征是用于限制类中可能值的逻辑属性了

其中string getAString()就不必多说,返回一个字符串,其中包含对应值

其中我发现一个很神奇事,不知该如何解释
我将代码中this=1改成1=this也会得到一样的结果,没有任何不同或报错
它和赋值语句不同,但好像又具有相似的功能
在对变量做限制时,例如where ott = 2,它就变成了一个符号,用于对两个值进行比较,这里还好理解,因为sql语法类似
但是同样在以下代码中

 predicate isEven() { // member predicate
      this = 2
    }

this=2也是用于对两个值进行比较
我认为这是由于predicate带来的改变,使得其中的代码和where后的代码具有相同得到功能
如果有更好的见解,还不忘赐教

最后来写题
题目给了模板和提示
按照step8中的规则进行编写,exists第二个参数放上step8中的where条件
由于select由题目给出并为Expr的子类,所以我们需要增加一个条件获取宏调用相关表达式
根据以上exists案例可知,我们需要在mi.getExpr() = 后面写出他返回值的类型,这样当mi为表达式时,就会被选中
NetworkByteSwapExpr的子类,因此

9_class_network_byteswap.ql

import cpp

class NetworkByteSwap extends Expr {
    NetworkByteSwap() { 
        exists(MacroInvocation mi | mi.getMacro().getName().regexpMatch("ntoh.*") | mi.getExpr() = this)
     }
}

from NetworkByteSwap n
select n, "Network byte swap"

 

Step 10 – Data flow and taint tracking analysis

最后一步,进行数据流分析

先了解以下我们需要查询的函数背景,ntoh*函数会返回一个数,并用于memcpy的第三个参数size,所以我们需要追踪的数据流就是从ntoh*memcpy

在C/C++写网络程序的时候,往往会遇到字节的网络顺序和主机顺序的问题。 这时就可能用到htons(), ntohl(), ntohs(),htons()这4个网络字节顺序与本地字节顺序之间的转换函数

memcpy指的是c和c++使用的内存拷贝函数,memcpy函数的功能是从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置中

创建Config类,查找此类的数据流并进行污染点追踪分析
进行数据流分析,我们需要用到,部分代码已经在给出的模板中

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

我们需要写两个predicate,一个是来源isSource,一个是接收器isSink

isSource中我们需要查询ntoh*宏定义调用的相关表达式,这一步我们已经在NetworkByteSwap中写过了
isSink中我们需要查询调用memcpy函数时,传入的第三个参数size,这一步我们需要新增加的步骤是获取参数

弄清楚这些后,在编写规则时,根据提示完善代码
我们就能获得10_taint_tracking.ql的答案

/**
 * @kind path-problem
 */

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

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

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

  override predicate isSource(DataFlow::Node source) {
    // TODO
    /*获取与此节点对应的表达式(如果有)。
    此谓词仅在表示表达式求值值的节点上具有结果。
    对于从表达式中流出的数据,例如通过引用传递参数时,请使用asDefiningArgument而不是asExpr。*/
    source.asExpr() instanceof NetworkByteSwap
  }
  override predicate isSink(DataFlow::Node sink) {
    // TODO
    exists(FunctionCall fc | fc.getTarget().hasName("memcpy") | 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"

 

传送门

cpp规则语法说明
Java正则模式
给出的参考案例:CVE-2018-4259: MacOS NFS vulnerabilties lead to kernel RCE(知识点挺多的)
codeql-swing(swing的语言云淡风轻,条理清晰,如沐春风,我的的语言阿巴阿巴阿巴)
讨论区

(完)