【技术分享】CodeQL U-Boot Challenge(C/C++)

admin 2025年7月3日02:57:52评论4 views字数 7628阅读25分25秒阅读模式

【技术分享】CodeQL U-Boot Challenge(C/C++)

背 景

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

放上题目链接

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

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 cppfrom Function f1where f1.getName() = "strlen"select f1,"a function named strlen"

【技术分享】CodeQL U-Boot Challenge(C/C++)

直接在main提交

【技术分享】CodeQL U-Boot Challenge(C/C++)

commit中查看结果,通过

【技术分享】CodeQL U-Boot Challenge(C/C++)

Anatomy of a query

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

import cppfrom Function fwhere f.getName() = "memcpy"select f,"a function named memcpy"

提交查看结果,通过

【技术分享】CodeQL U-Boot Challenge(C/C++)

Using different classes and their predicates

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

利用正则表达式

string类有一个方法regexpMatch,接收器将参数与正则表达式匹配

那我们需要先找到宏定义,再对该字符串进行正则匹配(使用的java的匹配模式)

5_function_definitions.ql

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

运行

【技术分享】CodeQL U-Boot Challenge(C/C++)

使用集合表达式
给出的格式:<your_variable_name> in [“bar”, “baz”, “quux”]
import cppfrom Macro mwhere m.getName() in ["ntohs","ntohl","ntohll"]select m,"macros named ntohs, ntohl or ntohll"

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

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

Relating two variables

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

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

import cppfrom FunctionCall call, Function fcnwhere  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 c2where  c1.getClass2() = c2 and  c2.getProp() = "something"select c1from Class1 c1where c1.getClass2().getProp() = "something"select c1

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

6_memcpy_calls.ql

import cppfrom FunctionCall functioncallwhere functioncall.getTarget().hasName("memcpy")select functioncall

提交通过

【技术分享】CodeQL U-Boot Challenge(C/C++)

Relating two variables,continued

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

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

【技术分享】CodeQL U-Boot Challenge(C/C++)

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

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

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

提交通过

【技术分享】CodeQL U-Boot Challenge(C/C++)

Changing the selected output

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

【技术分享】CodeQL U-Boot Challenge(C/C++)

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

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

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

【技术分享】CodeQL U-Boot Challenge(C/C++)

提交通过

【技术分享】CodeQL U-Boot Challenge(C/C++)

那么查询表达式和查询调用的区别是啥?
看注释说明,
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()的结果集
这里放上
语句和表达式的区别讨论链接

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 twhere exists(string c | t.getHairColor() = c)select t/*在CodeQL中,以下代码功能同于以上代码,给出只是为了更好地理解*/from Person t, string cwhere t.getHairColor() = cselect 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

【技术分享】CodeQL U-Boot Challenge(C/C++)

其中还有一个熟悉的单词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

运行截图:

【技术分享】CodeQL U-Boot Challenge(C/C++)

再更改规则如下:

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

他们会得到相同的结果

【技术分享】CodeQL U-Boot Challenge(C/C++)

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

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

【技术分享】CodeQL U-Boot Challenge(C/C++)

其中我发现一个很神奇事,不知该如何解释
我将代码中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为表达式时,就会被选中
NetworkByteSwap是Expr的子类,因此

9_class_network_byteswap.ql

import cppclass NetworkByteSwap extends Expr {    NetworkByteSwap() {         exists(MacroInvocation mi | mi.getMacro().getName().regexpMatch("ntoh.*") | mi.getExpr() = this)     }}from NetworkByteSwap nselect n, "Network byte swap"
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.TaintTrackingimport DataFlow::PathGraph

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

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

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

/** * @kind path-problem */import cppimport semmle.code.cpp.dataflow.TaintTrackingimport DataFlow::PathGraphclass 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 sinkwhere cfg.hasFlowPath(source, sink)select sink, source, sink, "Network byte swap flows to memcpy"
【技术分享】CodeQL U-Boot Challenge(C/C++)
- 结尾 -
精彩推荐
【技术分享】x86系统调用(上)
【技术分享】针对Office宏病毒的高级检测
【技术分享】Tomcat 内存马技术分析— Filter型
【技术分享】CodeQL U-Boot Challenge(C/C++)
戳“阅读原文”查看更多内容

原文始发于微信公众号(安全客):【技术分享】CodeQL U-Boot Challenge(C/C++)

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年7月3日02:57:52
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   【技术分享】CodeQL U-Boot Challenge(C/C++)https://cn-sec.com/archives/803108.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息