国际前沿技术 AI+Fuzz 实现细节(CKGFuzzer)
默文 AI专栏 1566浏览 · 2025-04-21 07:06

国际前沿技术 AI+Fuzz 实现细节(CKGFuzzer)

前言

本文分析仅代表个人看法,如有错误请指教,好的项目值得深入,如果你也对 LLM + Fuzzer 感兴趣,CKGFuzzer 是个不错的研究项目。

本文 Fuzz 针对库函数,实现细节分析的项目基于 CKGFuzzer ,该项目的论文即将要发表于 2025ICSE 会议的论文 CKGFuzzer,它通过结合代码知识图谱,让大语言模型可以更加高效且准确地生成模糊驱动器。

论文地址:CKGFuzzer: LLM-Based Fuzz Driver Generation Enhanced By Code Knowledge Graph

项目地址:security-pride/CKGFuzzer: CKGFuzzer: LLM-Based Fuzz Driver Generation Enhanced By Code Knowledge Graph

项目详细注释+Patch版本:Kernel/CKGFuzzer_mowen注释版 at master · mowenroot/Kernel

作者二开版本:mowenroot/AiLibFuzzer: LLM + Fuzzer

CKGFuzzer 是一个将多智能体系统与代码知识图谱相结合的模糊驱动器生成框架,其目标是针对 API 组合生成高效的模糊驱动器,以提升模糊测试的质量和覆盖率。

图片加载失败


大致工作流:

1、解析被测目标及其库 API,提取并嵌入代码知识图谱,包括解析抽象语法树,提取数据结构、函数签名、调用关系等关键信息。 2、查询代码知识图谱的 API 组合,关注那些具有调用关系的 API,生成相应的模糊驱动器。 3、编译生成的模糊驱动器,并且通过一个动态更新的库使用情况,修复出现的编译错误。 4、执行编译成功的模糊驱动器,监控库文件的代码覆盖率,对未能覆盖的新路径 API 组合进行变异,迭代该过程并持续进行。 5、使用链式推理分析在模糊测试过程中产生的崩溃,参考包含了真实 CWE 相关的源码示例,来验证这些崩溃的有效性。

CKGFuzzer 好处显而易见,优点也很多,相比于现有的方法(如 PromptFuzz)通过不同的 API 变异组合利用 LLM 生成模糊驱动器,甚至手动编码CKGFuzzer 能够基于代码知识图的 LLM 驱动的模糊驱动器代理,指导 LLM 为模糊驱动器生成更高质量的 API 组合和基于覆盖引导的变异。

但是详细分析完也是存在一定缺点的:

1、使用的一些库是相对老的、codeql 也是基于老版本,ql 采用老版本编写的。

2、文件目录处理不清晰,代码和项目会混淆在一起,如果你不是很熟悉该项目,可能会被他的文件夹搞混。

3、使用 LLM 时确实采用了记忆的功能,但是没有切片的处理,在实际测试中,利用 LLM 生成 API 描述信息的时候,要发送的信息量太大,会导致tokens 不足。

4、项目存在一定的 BUG,在本文搜索 Patch 可以看到,其实也不能算是 BUG,只是在测试中,环境不一致导致的不兼容。

5、多组 API 时,采用单进程进行 Fuzz,耗时会成倍的增加,这里是可以采用多线程处理的,即使 pythonGil 锁。也能大幅度提升效率。

代码执行流程,官方提供了三步骤:

1、repo

从 Target 库中提取信息

2、preproc

构建外部知识库

3、fuzzing

运行模糊测试进程

整体流体大致如下,这里做了简化实际执行没有这么简单,但是总体逻辑大致一样:

图片加载失败


分析也采用三步走,下面开始万字的详细分析底层实现,因为 CKGFuzzer 的核心就是 LLM 操作生成高质量的 target_fuzzFuzz 的覆盖率回调的 API 优化,所有会针对相关操作细致化分析,其他部分会粗略过。

repo

10 线程下整个流程大概花了 3小时40分钟

图片加载失败


文件位于



1、add_codeql_to_path()

添加codeql到环境变量中

图片加载失败


这里有个小bug,已经修复


Patch

原程序只有 ../ ,会导致定位不到外层的 docker_shared , 只会在fuzzing_llm_engine

图片加载失败



2、main()

1.setup_parser()

获取传入参数

图片加载失败


2.RepositoryAgent()

初始化该 RepositoryAgent 类,完成以下事情.

「1」 维护该类中的字段,

「2」 调用 check_create_folder() 检查输出目录是否存在,如果不存在则新建

「3」 调用 self.init_repo() 使用提供的参数初始化存储库。

将repo添加到数据库代码中。

提取API信息。

init_repo

「1」 init_repo() 会先检查codeqldb是否存在,以 .successfully_created 文件来判断,如果该文件存在则表示codeqldb存在。如果不存在调用 self._add_local_repo_to_database() 开始创建 database

「2」 如果当 src_folder 源码目录不存在时,会调用 self.copy_source_code_fromDocker() ,从docker中复制数据出来。

_add_local_repo_to_database

「1」 使用 getpass.getuse() 获取当前用户名,用于后面更改docker 中的文件所有者,因为文件映射中docker权限为root。

「2」 获取项目目录, 目录指向 fuzzing_llm_engine/projects/{project_name},该项目的目录下面包括 build.sh dockerfile project.yaml三个文件,用于构建环境使用。

「3」 维护 image_name 、 build_command变量。 image_name 为 docker的img名称,一般为

"项目name"+"_base_image",build_command 为 构建 docker 的命令。然后执行 build_command 开始构建 docker 镜像

未检查 docker_img 是否存在,这里增加检测

构建可以成功,但是可能会出现两个警告,

图片加载失败


警告原因因为,WORKDIR c-ares 未使用绝对目录,建议使用绝对目录。

图片加载失败


ENV OLD_LLVMPASS 1 的设置 应该为 ENV OLD_LLVMPASS=1



「4」 在 docker 中 创建 codql 数据库,通过以下命令拉起docker 并构建codeql数据库,这里执行语句带入了我本机的目录,方便理解。并在创建完 codeql database 后,在docker_shared/codeqldb/项目 目录下 创建 .successfully_created 文件来表示数据库创建成功。


可能会因为 /usr/local/bin/compile: line 27: FUZZING_LANGUAGE: unbound variable 报错 ,

图片加载失败


需要加上 -e FUZZING_LANGUAGE={args.language}

图片加载失败


这里为了方便调试我使用 docker保活,然后进去看docker配置

这个时候可能还有问题,提示没有codeql这个目录,这里我直接用脚本添加,当然也可以手动添加

图片加载失败



主要做了,-v 映射文件夹到docker,-e 指定了环境变量, -t 指定启动镜像, -c 执行了codeql 创建数据库语句。

在使用codeql 创建数据库时,编译语句使用 /src/fuzzing_os/wrapper.sh c-ares

wrapper.sh

wrapper主要负责 定位到项目源代码处,并对 build.sh 脚本做了检查,然后运行 build.sh 脚本。

build.sh

就是针对每个项目进行客制化编译了



3.extract_api_from_head

当 --src_api 存在时,会调用 extract_api_from_head()

图片加载失败


「1」 先判断项目源码是否存在,如果不存在需要从docker中复制出来

「2」 调用find_cpp_head_files()遍历并收集项目下面的所有 c/cpp 和 head文件,src_dic 为 项目中的头文件 + c/cpp文件,test_dic 为 项目中的测试文件 。

「3」 调用_extract_API() 分别为两个文件字典提取API信息,最后保存该文件列表{self.output_results_folder}/api/到json中。

find_cpp_head_files()

「1」 遍历项目中的所有文件,搜集 头文件和 c/cpp 文件,并区分是否包含test,区别是否包含test使用 check_path_test()函数。



_extract_API

「1」 利用上一步 find_cpp_head_files() 收集的文件进行遍历,这里做了一个兼容性读取文件内容。

「2」 利用CppParser.split_code()提取文件中的相应结构,is_return_node: 决定是否返回抽象语法树(AST)节点对象(True 返回 AST 节点,False 返回可序列化信息,如字符串 + 位置)。

fn_def_list: 函数定义列表

fn_declaraion: 函数声明列表

class_node_list: 类定义

struct_node_list: 结构体定义

include_list: 包含的头文件

global_variables: 全局变量

enumerate_node_list: 枚举类型b

「3」将返回的数据保存到同目录的{src}.debug.json文件中。

4.extract_src_test_api_call_graph()

如果制定了 call_graph 则会调用 extract_src_test_api_call_graph(),你会发现还会调用 extract_api_from_head() ,所以指定了 call_graph 就可以不用使用 --src_api

图片加载失败



报错在运行 CodeQL 查询时,缺少了一个关键的依赖包:codeql/cpp-all,而且没有对应的 lock 文件来自动安装。

图片加载失败


查看了qlack.ymal的格式使用的是libraryPathDependencies,是很老版本使用的,所以这里需要把SDK+引擎都降级为 v2.7.3

引擎地址:Releases · github/codeql-cli-binaries

SDK地址:Release codeql-cli/v2.7.3 · github/codeql

当 SDK+引擎 都降级后才能使用源代码正常执行,否则需要重写 ql 很麻烦。

图片加载失败



extract_src_test_api_call_graph

作用:提取源码和测试代码中的 API 调用图(Call Graph)

「1」 根据之前 extract_api_from_head() 提出出来的api文件,把每个文件中的fn_def_list(函数定义列表)提取出来。

「2」 构造一个任务列表 eggs,中包含每个函数所需的信息,[函数名,文件路径,CodeQL 数据库路径,输出结果文件夹路径,LLM 工作目录路径]

「3」 因为整个项目太大了,使用codeql 提取出调用关系又慢,所以必须要使用多线程,使用多线程需要为每个线程拷贝一份数据库,然后并发运行 handle_extract_api_call_graph_multiple_path 函数,执行完后删除拷贝用的临时数据库。

handle_extract_api_call_graph_multiple_path

该函数作用主要就是 _extract_call_graph() 的封装

_extract_call_graph

作用:遍历 eggs 并调用 extract_call_graph.sh 从中提取所有函数的调用关系。并将 bqrs 转换为 csv。

extract_call_graph.sh

做了一些初始化,核心语句就三句

执行这个脚本后就可以使用 codeql 查询所有文件中的所有函数的调用关系以供后面输出fuzz的target文件。

大致会类似执行以下语句

执行完成后 fuzzing_llm_engine/external_database/{项目名称}/call_garph 下会有 每个函数的调用关系的bqrs和csv。

图片加载失败


preproc

用来构建外部知识库

文件位于 : ./fuzzing_llm_engine/repo/preproc.py

main

「1」 设置传参格式,project_name(项目文件名)、src_api_file_path(external_database/{项目名}的位置)

「2」 通过用户指定的api_list使用三个函数进行聚合信息, combine_call_graph 聚合 graph 关系, extract_api_from_file 聚合 api 基本信息(函数名、函数所在文件),extract_fn_code 聚合函数代码。

经过聚合后会在 api_combine 中产生三个对应的聚合文件。

图片加载失败


setup_parser

就只有两个参数,一个项目名称,还有一个为上一步 repo.py 提取出来的 api 文件所在处()。

图片加载失败




combine_call_graph

就是聚合作用,把用户指定的 api 所对应函数的调用关系全部聚合在一个新的文件中。

「1」 遍历 codebase/call_graph 文件夹中所有以 .csv 结尾的文件,根据用户提供的 api_list.json 来筛选出匹配的 CSV 文件。

「2」遍历匹配的 CSV 文件,通过 pd 库将多个csv 合并为一个并保存到 api_combine/combined_call_graph.csv



extract_api_from_file

也是聚合作用,与 combine_call_graph 不同的是,把之前提取的 api 信息进行聚合,聚合两个信息(函数名和函数所在的文件名)

「1」 遍历之前提取的 api 信息,根据用户提供的 api_list.json 来提取出 函数名、函数所在文件。

「2」 聚合 api 信息保存到 api_summary.json并拷贝至 api_combine 下, 现在 api_combine 目录下会有两个文件: graph 聚合、api 聚合。



extract_fn_code

也是聚合作用,和聚合 api 信息代码几乎相同,聚合了指定 api code



fuzzing

前置

图片加载失败


获取当前目录和项目根目录,添加项目根目录到环境变量中



这样可以导入不在当前目录或 site-packages 的模块

执行过程比较长,这里直接拆分,一步一步看。

1、传参处理

args_parser() 开始就是传参的设置

图片加载失败


以下是参数的大致介绍

参数名称
类型
默认值
描述
--yaml
字符串
""
指定 YAML 配置文件的路径或内容
--gen_driver
布尔类型
True
是否生成模糊测试驱动
--gen_input
布尔类型
True
是否生成输入
--summary_api
布尔类型
True
是否启用 Summary API
--check_compilation
布尔类型
True
是否检查编译
--skip_check_compilation
布尔类型
False
是否跳过编译检查 (会覆盖 --check_compilation)
--skip_gen_driver
布尔类型
False
是否跳过生成模糊测试驱动 (会覆盖 --gen_driver)
--skip_gen_input
布尔类型
False
是否跳过生成输入 (会覆盖 --gen_input)
--skip_summary_api
布尔类型
False
是否跳过 Summary API (会覆盖 --summary_api)

参数处了从用户传入,还有一个从 config.yaml 文件传入。

需要注意的是:该项目是基于 OSS-Fuzz 之后有很多包含 OSS-Fuzz 的基本操作,包括这步的 config.yaml 也是 OSS-Fuzz 的基本配置文件。

config.yaml

便于之后利用这里放上 config.yaml 的具体配置

图片加载失败


2、获取 Agent

Agent 分为两大类就是 :生成文本的语言模型、文本向量化模型。

「1」 通过 get_model() 获取 config.yaml 配置中的对应的 文本语言模型,模型有 coderanalyzer 两类。

「2」 通过 get_embedding_model() 获取文本嵌入模型,该模型之后用来进行文本向量化。

「3」 设置全局 SettingsLLM 设置。Settings 是来自 llama-index里的一个模块,后续的 llm 就会被内部组件自动调用。

因为模型有 coderanalyzer 两类,所以在调用时会先行判断需要的版本是否存在,如果不存在则获取另一个版本。

get_model

「1」 默认使用 llama3:70b ,可选择的模型有: deepseek、openai、ollama

「2」 通过 OpenAILike、Ollama 返回 封装了大语言模型服务的客户端类,是 对象实例 ,后续可对该对象直接进行操作。

get_embedding_model

「1」 默认使用本地部署的 BAAI/bge-small-en-v1.5

「2」 调用 HuggingFaceEmbeddingOpenAIEmbeddingOllamaEmbedding :返回是 对象实例 用来语义搜索、RAG 等任务

PS :这里的 device 是我修改过的,使用 cpu 进行驱动,原本是使用 cuda:1

Patch


因为WSL+win10低版本的各种问题,这里我不选择使用 CUDA,需要修改代码使用 CPU 来进行文件向量化。

图片加载失败


图片加载失败



3、索引存储

「1」 test 文件的索引存储 ,以修复生成的模糊驱动程序

「2」 CWE 文件的索引存储

图片加载失败


build_test_query

「1」 先设置 Settingsllm , 后续会隐式调用 llm

「2」 确定数据和索引目录路径、如果索引已经存在,就加载它。使用 StorageContext.from_defaults() 创建一个加载上下文;

调用 load_index_from_storage() 从本地加载已保存的向量索引;

「3」 否则,构建新的向量索引。使用 SimpleDirectoryReader 加载目录下的所有文件(作为文档对象);使用 SentenceSplitter 把长文档拆成小段(chunk):chunk_size=512:每段最多 512 字符、chunk_overlap=30:相邻段之间有 30 字符重叠。然后调用 VectorStoreIndex.from_documents() 创建索引:会用 Settings.embed_model 来将每段文本转为向量;索引构建完成后,用 .persist() 将构建好的索引持久化到指定目录,方便下次直接加载。


构建好的效果图如下:

图片加载失败


图片加载失败


这样就在 test_case_index 下就会生成 test 文件 的存储向量索引

图片加载失败



build_cwe_query

和索引存储 test 文件逻辑一致


构建好的效果图如下:

图片加载失败


cwe 的对应向量索引

图片加载失败



4、API 分析(skip)

这步可使用 skip_summary_api 跳过。

「1」 获取 API 列表 ,初始化每个 API 的使用次数为 0

「2」 创建 FuzzingPlanner 对象 , 这个对象会负责后续 API 相关操作。

「3」 (args.summary_api) 判断是否需要重新生成 API 摘要。

├── 是:调用 LLM 分析代码 ➝ summarize_code() └── 否:直接使用已有摘要文件

两个分支的区别只有,是否调用 summarize_code()

「4」 将 API 摘要文件复制到 api_combine 目录、通过 extract_api_list 提取函数名列表(确保是带摘要信息的),加载完整的 API 源代码文件和 API 摘要文件


这步做的就是给 API 生成,函数的摘要,对应文件的摘要 。

图片加载失败



summarize_code()

「1」 载入之前聚合的 api.json code.json

「2」 遍历每个文件和函数,跳过已经生成过摘要的函数

「3」 找出该 APIgraph 中的所有的调用关系(调用者、或者被调用者) ,把 graphAPI code 调用 get_code_summaryLLM 生成函数摘要。

「4」 跳过已经存在的文件摘要,把文件下所有函数的摘要合并,调用 get_file_summary()LLM 生成一个 "文件整体描述"。

find_call_graph_with_api

「1」 遍历图数据的每一行,把该 API **被调用 **或者 调用 的关系全部提取出来。

get_code_summary

「1」 将 api code(api_info) 、api graph(call_graph)、api name(api) 三个信息套在 prompt 中,然后调用 LLM 。

code_summary_prompt 的提示词,主要是说:这是什么函数名称,API 信息,调用图中的字段。

请根据以上信息,为该函数生成一段不超过 60 个词的代码摘要,内容需覆盖以下两个方面:

1、函数的主要功能。2、函数的使用场景

get_fie_summary

「1」 将 文件名 和 文件下所有函数的摘要合并信息 ( file_info ) 套在 prompt 中,然后调用 LLM 。

file_summary_prompt 提示词


以下是一个 JSON 文件,包含了一个项目文件中的所有 API 信息: {file}

每个 API 名称后面紧跟着其对应的代码摘要: {file_info}

请基于每个文件中包含的 API 的代码摘要,为每个文件生成一段不超过 50 个词的文件摘要,内容需涵盖以下两个方面:

1文件的主要功能;

2文件的使用场景。

请翻译成以下格式: File Summary: <你的摘要>


Patch


需要清楚的一点是 graph 很大,我提供了 4 个 API,提取出来的 graph.csv120MB ,相当大。行数更是恐怖的 28w 多行。

图片加载失败


即使只提取相关 APIgraph 行数也是巨大的,导致发送的 tokens 巨大,在源代码中是没有做切片处理的,但是 deepseektokens6w 多 ,测试的时候需要发送某个 API 的完整 graph tokens 都达到了 30w 多 ,导致报错。

图片加载失败


解决方案:

1、做切片处理

2、对内容进行缩减。

3、更换模型

因为只是 API 的描述信息,所以 graph 少的情况下,影响不算特别大,所以这里使用对 graph 行数进缩减,只发送前 50 行的调用关系。

图片加载失败





5、代码知识图谱(KG)

「1」 调用 build_kg_query() 构建知识图谱,返回值为 4 个语义检索索引,3个图谱索引(这里的file_summary图谱不需要),1 个元数据 code_base

对象名
描述
pg_all_code_index
所有代码的图谱索引
pg_api_summary_index
API 摘要的图谱索引
pg_api_code_index
代码的图谱索引
pg_file_summary_index
摘要文本的图谱索引
summary_text_vector_index
摘要文本的语义向量索引
all_src_code_vector_index
所有源码的语义向量索引
api_src_vector_index
所有 API 源码的向量索引
code_base
元信息

「2」 将这 4个 图谱索引对象转换为 BaseRetriever 对象,设置 similarity_top_k=3 表示每次最多返回 3 个相似结果,为后续对话、查询、代码摘要生成等提供**“上下文信息检索器”**。

「3」 创建一个 CodeGraphRetriever 支持混合多种信息源检索的对象,汇聚代码、摘要、结构等对象,并将 CodeGraphRetriever 设置到 FuzzingPlanner

mode="HYBRID":表示它将同时从多个信息源中选取最合适的检索结果(例如代码 + 摘要的混合搜索);

getCodeKG_CodeBase

build_kg_querygetCodeKG_CodeBase 的封装,这里直接看 getCodeKG_CodeBase()

「1」 提取元数据,类型为 CodeRepository ,通过 get_codebase 获取代码库信息,包含项目的结构、文件路径、函数定义等内容,并可根据 exclude_folder_list 排除部分文件夹

「2」 加载 API 摘要,判断是否初始化图谱。

「2.1」 初始化图谱:

[2.11] 基于graph调用图 CSV 和源码,构建代码调用图,并生成 5 类“文本节点”用于后续向量化:

all_src_text_nodes: 所有源代码的文本节点列表

summary_api_nodes: 每个 API 的摘要节点列表

file_summary_nodes: 文件摘要的文本节点列表

api_src_text_nodes: 每个 API 的源代码文本节点列表

[2.12] 对以上节点进行语义向量化(构建 Chroma 向量库)

[2.13] 构建 4 个属性图索引(Property Graph Index),每个索引代表一个不同视角的图谱,并结合对应的向量数据库,供后续查询使用。

[2.14] 持久化图谱索引:调用 .persist() 将图谱结构和向量嵌入结果写入磁盘,便于下次直接加载。

「2.2」 加载图谱,不初始化图谱,区别有:是否调用 getCodeCallKGGraph() ,配合已存在的 vector_storeproperty_graph_store 恢复 4 个图谱索引对象。

get_codebase

get_codebase 主要就是 CodeRepository.construct_nodes_fn_doc 的封装。

construct_nodes_fn_doc

从 API 数据(源码和头文件)中提取结构化的节点信息,包括文件、函数、结构体、枚举等,为后续构建图谱提供语义实体

「1」 获取源文件和头文件信息,获取项目路径前缀(这里是我Patch的部分,因为当路径为绝对地址,按照源代码的相对地址就会替换失败,导致后面全部提取失败,提取前缀增加兼容性。后续前缀会被替换为空,然后再将数据集合。)

「2」 遍历所有源文件和头文件,并读取文件内容(读取的时候做了编码兼容),使用 replace() 替换前缀(PS:这点很重要影响后续整个流程),构建文件节点字典 file_id_dict

「3」 调用 process_source_files() 提取源代码中的函数定义、全局变量、结构体、枚举节点 (src_fn_def_list_list, src_global_var_node_list, src_struct_node_list, src_enum_node_list)。

「4」 构建 结构体、枚举 定义与别名映射,最终两个字典形如:struct_def: struct 名字 -> struct 定义节点typdef_struct_alias: struct typedef别名 -> 原始 struct 名

「5」 构建函数定义映射 fn_dec,每个函数以 “文件路径-函数名” 为键,保存其定义信息。

process_source_files

从预处理后的代码结构信息中提取并标准化出“节点”数据(函数、结构体、枚举、全局变量),为后续图谱建模准备实体数据。

「1」 提取项目路径前缀(这里是我Patch的部分,因为当路径为绝对地址,按照源代码的相对地址就会替换失败,导致后面全部提取失败,提取前缀增加兼容性。后续前缀会被替换为空,然后再将数据集合。)

「2」 遍历所有文件解析每个文件的节点,并跳过指定排除的文件夹

「3」 构建 函数、结构体、枚举、全局变量 4 个节点信息(重点),标准化成 {fid}-{函数名} 的唯一 ID,附带代码和参数信息。

Patch


construct_nodes_fn_doc 函数中,使用 replace 来替换,这样其实可以抵消项目中有特殊符号导致的问题,但是当使用项目为c-ares 时,我们的路径又是绝对地址这样就会替换失败,导致后面全部提取失败

图片加载失败


上面代码在 fuzzing_llm_engine/rag/kg.py 中的 get_codebase() 被调用,原目的是替换路径用来构建源数据为图存储对象

图片加载失败


但在后面的 getCodeCallKGGraph() 中调用,使用 "-" 来分割文件名和文件函数,这里就有问题了,当路径为绝对地址不能被replace() 时,就会导致全部提取失败。

图片加载失败


解决方案:

1、替换 “-”,用其他更特殊的符号如 "$" 来区别

2、使用相对路径进行 api 的提取

3、修改 replace 函数代码

这里使用第三种方案,添加前缀获取并替换模式

construct_nodes_fn_doc() 函数中修改以下代码

图片加载失败


在该函数中还会调用 process_source_files() 里面也使用了类似逻辑

图片加载失败


修改如下

图片加载失败


修改效果,这样修改之后就能规避项目或者路径中自带的 - 干扰。

图片加载失败



get_or_construct_chromadb

「1」 创建一个持久化的 Chroma 客户端(连接到磁盘上的数据)

「2」 获取或创建指定名称的 collection(类似于数据库中的表)。

「3」 用 collection 构建 ChromaVectorStore, 封装 collection,变成 llama-index 中支持的向量存储接口。

「4.1」 初始化新的向量索引 :

设置 LLM 和嵌入模型、创建向量存储上下文、构建向量索引并写入 TextNodes

「4.2」 不初始化新的向量索引,从现有向量数据库加载索引。



getCodeCallKGGraph

根据函数调用关系和代码信息,构建一个用于图查询Knowledge Graph Query)的知识图谱,其中包括函数、文件、调用关系、函数摘要、源代码等多种信息,最终返回构建好的图谱及其相关节点集合

「1」 使用 pandas 读取 call_graph_csv 文件、初始化图存储对象。

「2」 初始化实体节点、关系和源代码块的列表,维护 methods_in_codebase(所有项目中定义的方法名) -> 遍历所有函数定义提取函数名。

「4」 逐行构建调用图中的实体节点和边,对每一条调用记录:

判断 caller 和 callee 是否为项目中的函数(否则视为库函数)

提取函数签名、源代码(如果有)

判断是否有 API 摘要信息并加入属性

为函数和文件创建实体节点 (EntityNode)

为调用关系创建边 (Relation):CALLS:函数调用、CONTAIN:文件包含函数。



「4」 插入节点和边到图谱

图片加载失败


「5」构建函数定义和摘要的文本节点 (TextNode),遍历所有函数定义 fn_def

如果该函数已添加到图谱中,则构建对应的 TextNode(纯文本节点)

all_src_text_nodes:所有函数代码节点

summary_api_nodes: 函数级摘要节点

file_summary_nodes:文件级摘要节点

API_src_text_nodes:摘要中提到的 API 函数的源码节点



代码知识图谱(KG)小结:

因为这块核心区域代码量相对来比较庞大,阅读起来理解起来相对比较复杂,但是他主要就做了如下几件事:

1、主要目的是为了: 获取图谱并持久化图谱索引

2、为了获取图谱就需要获取 **文本节点 **和 关系图,所以要先调用 getCodeCallKGGraph()获取。

3、获取文本节点,就需要代码库信息(项目的结构、文件路径、函数定义等内容),所以要先调用 get_codebase() 获取。

4、文本节点 就是各种字典的集合,关系图 分为 节点、边两个关系。

5、加载 4个 图谱索引,需要 关系图文本节点,所以会在获取 文本节点、关系图 后都存储起来。

在保存之后 kg 目录下会出现 4 个文件夹:

所有源代码的文本节点列表

API 摘要的文本节点列表

文件摘要的文本节点列表

API 源代码的文本节点列表

图片加载失败


对应四个 Chroma 向量数据库

图片加载失败


6、初始化各种 Agent

「1」 gen_agent -> 初始化 FuzzingGenerationAgent,用于生成模糊测试驱动程序

「2」 fix_agent -> 初始化 CompilationFixAgent,用于修复编译错误

「3」 input_agent -> 初始化 InputGenerationAgent,用于生成模糊测试输入

「3」 crash_analyze_agent -> 初始化 CrashAnalyzer,用于分析崩溃信息

7、生成 fuzz 程序(skip)

这步可使用 skip_gen_driver 跳过。

「1」 尝试获取或生成 API 组合,判断 api 文件是否存在 -> agents_results/api_combine.json,如果存在直接加载 API 组合。不过这个 API 联合体最开始是需要 LLM 来生成的,如果对项目熟悉的话,也是可以自己写 API 组合的。

「2」 生成 API 组合,调用 plan_agent.api_combination

「3」 生成 Fuzz 测试程序,调用 gen_agent.driver_gen()

「4」 设置 input_agentAPI 组合信息,用于后续输入生成阶段(生成输入值、调用链等)。

「5」 把 fuzz 驱动复制到 Docker 的共享目录。

api_combination

传入的 api_list 中每一个 API,使用 LLM 结合代码图谱信息,生成建议的 API 调用组合,返回多个组合结果列表。

「1」 创建查询引擎和响应格式化器;combine_query_engine: 支持混合检索模式的代码图谱查询器。response_format_program: 将 LLM 的原始自然语言回答格式化为结构化对象。

「2」 遍历输入的 API 列表,对每个 API 生成组合建议:

构造初始提问语句,提示词传入:当前要组合的 API;所有 API 列表;API 的使用频次信息(api_usage_count

检查是否启用上下文,如果开启则:将上下文拼接进新的提问语句中,形成增强型提问

combine_query_engine.query(question) 调用 LLM 进行代码知识图谱问答

response_format_program 再次调用 LLM 调用,将回答格式化为结构化对象

「3」 将本轮问答保存到向量记忆中,以便后续使用历史上下文

「4」 将当前 API 添加到组合结果中(这里需要注意的是:会将当前指定的 API 再次加入 API 组合中,所以之后想要使用该 API 组合就需要去重),更新 API 使用次数记录。

Patch


正常的回答应该是这样的

图片加载失败


但是有时候会这样回答,就导致报错

图片加载失败


解决方案:

1、添加历史记录以供参考

2、添加 try catch 循环处理报错

3、提供更好的 prompt

这里进行 prompt 重写

prompt 重写如下

图片加载失败


构造问题采用 api_combination_query_mowen

图片加载失败





driver_gen

根据 API 组合,利用 LLM 自动生成 fuzz driver 源代码文件,用于后续模糊测试。

「1」 遍历 API 组合列表 , 因为在生成 API 组合的时候,会将当前指定的 API 再次加入 API 组合中,所以这里,做了 API 的去重。

「2」 从 api_summary 中查找描述,从 api_code 中获取源码

「3」 调用 fuzz_driver_generation() ,根据提供的 API 信息(源码、摘要),项目名称 自动生成 fuzz driver。

「4」 从 LLM 返回内容(一般都为 Markdown格式)中提取出 C/C++ 代码。这里调用 extract_code() 利用正则匹配代码块即可。

fuzz_driver_generation

根据提供的 API 信息(源码、摘要),项目名称 自动生成 fuzz driver。



8、检查编译(skip)

这步可使用 skip_check_compilation 跳过。

「1」先启动检查编译使用的 docker。完成这步 docker 已经启动了,并且已经编译好了 libfuzzer 和项目。接下来就会对 target_fuzz 程序进行编译检测。

「2」调用check_compilation() 来检查之前生成的 fuzz 程序是否能过编译。

检查编译过程展示

处理编译报错的情况

图片加载失败


经过多个 LLM 处理后给出的 fix 结果

图片加载失败


有 5 次试错机会,第一次就 fix 成功

图片加载失败


生成的其他样本都没有问题,编译可以直接通过

图片加载失败




start_docker_for_check_compilation


PS:相关于 Docker 的操作基本都是用的 OSS-FUZZ 框架,师傅们这边可以选择跳过。

构建脚本为 oss-fuzz 中的 infra/helper.py

google/oss-fuzz: OSS-Fuzz - continuous fuzzing for open source software.

OSS-Fuzz 使用:Code coverage | OSS-Fuzz


启动一个 Docker 准备检查 fuzz 代码是否可以成功编译

「1」先检查 project_name 的容器是否正在运行,检测语句形如:

「2」如果容器不在运行,则利用 check_gen_fuzzer.py 脚本启动 docker

check_gen_fuzzer.py 中通过 start_docker_check_compilation 会调用到 start_docker_daemon()

图片加载失败


start_docker_check_compilation_impl

start_docker_daemon()start_docker_check_compilation_impl() 的封装,所以我们直接看 start_docker_check_compilation_impl()

在指定架构下,配置并启动一个 Docker 容器,根据提供的 fuzzing 引擎和 sanitizer,对给定项目进行构建验证(编译 check)。

「1」 构建项目镜像(拉取),会指定项目中的 Dockerfile 进行拉取

「2」 处理 sanitizer、这里指定的 sanitizer 就是 libfuzzer 中的 fsanitize 参数。

「3」 构建环境变量,因为环境变量使用时需要前置 -e ,通过调用 _env_to_docker_args() 转换为以下类似的语法方便调用:

['-e', 'SANITIZER=address', ...]

环境变量名
示例值
作用说明
备注
FUZZING_ENGINE
libfuzzer
指定使用的模糊测试引擎,用于控制编译逻辑和运行方式。
如:libfuzzerafl++
SANITIZER
address
使用的 Sanitizer 类型,用于运行时检测内存/未定义行为错误。
可多个组合,如 address,undefined
ARCHITECTURE
x86_64
架构类型,决定在何种硬件平台或模拟器上进行构建和运行。
如:x86_64aarch64
PROJECT_NAME
c-ares
当前项目名称,在路径、日志等构建中使用。
与项目结构密切相关
HELPER
True
标记该容器为辅助容器(如用于构建/验证),而不是执行 fuzz 测试的主容器。
控制某些构建逻辑分支

「4」 配置挂载文件夹,这里默认会挂载三个文件夹:out、work、generated_fuzzer

本地路径(宿主机)
容器内路径
目录类型
作用说明
fuzzing_llm_engine/build/out/{project}/
/out
输出目录
用于保存编译后生成的 fuzzing 二进制文件(如 fuzzer 可执行程序) fuzz的各种结果。并且包含 work 目录
fuzzing_llm_engine/build/work/{project}/
/work
工作目录
构建过程中fuzz程序的中间.o文件,并存放用于fuzzer的种子。
docker_shared/
/generated_fuzzer
输入目录
存放一些构建脚本等,供容器内编译脚本读取并集成进项目。

「5」 调用 docker_start() 执行配置好的语句,来启动 Docker 。下面是一个测试样例。

「6」 调用 docker_exec_command() , 执行 compile 命令 ,编译项目 -> /src/<project>/build.sh

到此 docker 已经启动了,并且已经编译好了 libfuzzer 和项目。接下来就会对 target_fuzz 程序进行编译检测。



check_compilation

用于检查目录下 fuzz 驱动文件的编译情况,并自动尝试修复

整个函数执行的流程还是比较清晰的,大致流程图如下。

图片加载失败


「1」 创建编译通过的目录 -> compilation_pass_rag。完成其他目录的初始化工作。

「2」 遍历所有的 fuzz 驱动程序,需要跳过 fix 文件,文件后缀不为 .c\.cctarget_fuzz

「3」 调用 fuzzing_llm_engine/utils/check_gen_fuzzer.py 中的 run() 函数来编译 target_fuzz(最终会执行下面的脚本,就是对之前启动的 docker 执行编译语句),根据编译返回的结果中是否有 error 的字样来判断是否编译失败。

「4」 如果编译不通过,则调用 fix_compilation() 来使用 LLM 自动修复,修复后还会判断是否编译失败,默认编译失败上限为 5 次。超过 5 次就会抛弃该测试样例。

「5」 对于编译通过的,会将编译通过的文件拷贝至 compilation_pass_rag 以供后续测试使用。

fix_compilation

来看看 LLM 是怎么进行自动修复的。这里需要注意的是,初始化 fix_agent 的时候是带有编译成功的文件索引的。当提问的时候,会检索 query_tools 中相关索引,比如检索 test_case_index 中最相关的 fuzz 测试函数片段。

图片加载失败


「1」 第一次调用 LLM 提取错误信息对应的代码片段。把错误片段格式化成新的问题,如果使用提取错误信息失败,则使用原始的错误信息。

「2」 第二次调用 LLM 用来先生成一个 example ,这步并不会修复,只是生成了一个修复样例,好处就是容错性更好了。

「3」 第三次调用 LLM 生成修复代码,这步返回真正的 fix code ,只需要提取返回 Markdown 中的代码,就拿到最终修复的代码了。

Patch


执行 check_compilation.sh 脚本 来检查编译是否通过

图片加载失败


由该脚本又引发一个问题,脚本中固定文件后缀为 .c 拷贝到 /src/{project}/test/ 下然后在进行编译检查

图片加载失败


但是生成的 fuzz 程序后缀为 .cc ,所以就会造成拷贝失败

图片加载失败


解决方案:

1、取消 sh 脚本中的固定 .c 后缀,对应的在脚本中也不要去除后缀即可正常拷贝编译

图片加载失败


2、修改 sh 脚本中后缀为 .cc




9、生成输入种子(skip)

这步可使用 skip_gen_input 跳过。

「1」 调用 input_agent.generate_input_fuzz_driver()Agent 生成尽可能覆盖多路径的种子。

generate_input_fuzz_driver

读取源码文件,生成一个用于 fuzz 测试的初始输入 seed,并将其保存到指定目录中

「1」 遍历所有能够编译成功的 target_fuzz

「2」 调用 generate_input() 根据源代码和提取的 ID ( 这里提供的 ID 对应 API 组合)生成一个 fuzz 测试初始输入种子。

「3」 保存种子到 {fuzzer_name}_corpus 文件夹中。

generate_input

根据源码、静态分析数据流图(DFG)和相关 API 生成用于 Fuzz 的输入种子

「1」 调用 dfg_analysis() 通过 LLM 对源代码分析得到 DFG,挖掘出代码的数据依赖结构。LLM 后续可以基于 DFG 理解代码结构与变量流向。

「2」 通过 dfgsource_codeapi_signature 构造 prompt ,然后发送给 LLM ,让其生成 raw 的输入种子

「3」 让 LLM 重新格式化这个 raw 输出,提取出 input + reason,如果失败,就把 raw 文本解析为 InputSeed 对象,并降级使用原始输出。

图片加载失败


10、Fuzz

在完成以上所有准备工作之后,项目终于进入了 fuzz 阶段。如果此时只是简单地对之前生成的 target_fuzz 进行表面包装来开展 fuzz 测试,那么整个项目的质量势必会大打折扣。因为对于这个项目而言,fuzz 阶段的成效在很大程度上依赖于前期所做的大量准备工作,正是这些准备确保了能够生成高质量的 target_fuzz,进而决定了最终是否能触发 crash

CKGFuzzer 的不同之处在于,它不仅进行分支覆盖率的统计,还能基于覆盖率判断是否需要对 API 进行优化。优化后的 API 会重新经过前面的所有流程,再用于新一轮的 fuzzCKGFuzzer 的核心优势就在于极致地利用了 LLM 来生成高质量的测试程序,从而显著提升了整个项目的质量。

「1」 初始化 Fuzzer ,几乎把之前的所有资源全部都融合进去了。调用 build_and_fuzz() 开始 fuzz。

图片加载失败


build_and_fuzz

「1」 遍历每个 fuzz 驱动程序,调用 build_and_fuzz_one_file() 对每个程序进行单独的 fuzz

其实对于一个项目进行 fuzz 的定时基本不会小于 24 小时,所以要对所有 fuzz 驱动程序跑完整个流程,耗时也是成倍增长,所以这里使用 build_and_fuzz_one_file() 最好是多线程的批处理。能够极高提升 fuzz 的效率。

build_and_fuzz_one_file

「1」 初始化共组,获取 API 组合,确认 fix fuzz driver 文件夹存在,

「2」 编译 fuzz driver 调用 check_gen_fuzzer.py,最终会执行以下的命令,使用之前启动的 c-ares_check 的 Docker 作为编译环境,再调用 entrancy.sh 进行 taget_fuzz 编译的准备工作,为什么是说准备工作,后续会分析这个脚本,然后执行 compile 开始编译。然后这里编译失败,会直接跳过该文件,该阶段没有 fix

「3」 启动 fuzzer ,调用语句如下,使用 config.yaml 中的 time_budget 来控制单个 fuzz 的最长时间( timeout ),默认的镜像采用 gcr.io/oss-fuzz-base/base-runner

「4」 如果有 crash 的产生,则有 ERROR 的字样,所以通过检测返回字符串是否有 ERROR 来判断是否有crash。


有个不好的点就是,因为使用的是 check_output 无法实时显示 fuzz 的过程,我们可以使用流来实时显示输出。

图片加载失败



「5」 生成覆盖率报告,这里需要分为三步,

第一步:构建 fuzz driver 的覆盖率版本(使用 --sanitizer coverage,并且这里还需要拉取 Docker 镜像)。总体就是执行了下面两段命令。

第二步:使用 coverage 命令收集并生成覆盖率报告。这里都是基于 0SS-FUZZ 的。默认生成的报告为 HTML 形式。

第三步:将 HTML 报告转换为可分析文本格式(html2txt()),使用 update_coverage_report() 更新和比较当前覆盖率。update_coverage_report() 调用极为关键,会通过分支覆盖率,判断是否触发新的分支, 如果没有新分支被覆盖,尝试重新 fuzz。

「6」 回调尝试提升覆盖率(多轮尝试,默认 3 次),就是重新完成之前完成的所有步骤:低覆盖率的 API、生成 API 组合、生成新的 fuzz driver 文件、尝试编译新的 fuzz driver 文件并尝试 fix、生成输入 fuzz driver、运行 fuzz driver、生成覆盖率报告、判断是触发新分支。

所有的 fuzz 操作都是基于 0SS-FUZZ ,并且是套其他 fuzzer 进行测试,就是几条命令的执行,这里不展开详细分析,下面重点分析 CKGFuzzer 是如何回调尝试提升覆盖率的。

html2txt

通过 0SS-FUZZ 提供的 Coverage ReportHTML 形式的,方便用户看,但是需要处理判断数据就需要提取数据。 所以 html2txt() 函数主要作用就是把 HTML 中的数据提取出来。

图片加载失败


默认生成 Coverage Report

图片加载失败


「1」 遍历所有符合条件的 HTML 文件(.c.html, .cc.html, .cpp.html),且不包含 "fuzz_driver" 关键词的文件。使用 BeautifulSoup 解析 HTML 内容,因为覆盖率信息一般保存在表格中,所以会查找所有的 table 元素,遍历每一行把数据提取出来,将提取后的文本写入到对应的txt 文件中。

目录结构如下,每个 target_fuzz 都会有一个专门的文件,这里比较重要的是 merge_report ,该目录为覆盖率合并目录,把所有 target_fuzz 的覆盖率信息全部合并。这个文件夹是之后回调的关键。

图片加载失败


update_coverage_report

「1」 这里的参数 merge_dir 就是之前提到的累计合并覆盖率报告目录new_report_dir 为每次单个 target_fuzz 生成的覆盖率报告。

「2」 会先创建临时目录 temp_dir ,将 new_report_dirmerge_dir 在临时目录 temp_dir 中合并。再进行 temp_dirmerge_dir 的覆盖率比较,以此判断新产生的覆盖率是否比之前已有数据更全面,分支更多。

「3」 调用 calculate_line_coverage() 用来收集行覆盖率、调用 calculate_branch_coverage() 用来收集分支覆盖率。calculate_files_branch_coverages 计算每个文件的分支覆盖率。这里的行覆盖率就是代码行数,分支覆盖率就是分支次数(分支:if,switch等)。

「4」 判断是否有新的分支被覆盖(核心判断标准),如果发现新的分支被覆盖,更新 merge_dir(用临时目录覆盖原目录),并返回 True。返回的 bool 决定之后是否回调然后重新 fuzz



calculate_line_coverage

「1」 计算行覆盖率还是比较简单的,遍历文件夹下面的所有 txt 文件,提取出 代码行定位 + 覆盖次数 + 代码 ,如果覆盖次数不是 0 ,说明这一行被覆盖了,就累加起来。

「2」 这里行覆盖率不同于覆盖次数,无论覆盖次数为多少,只要被覆盖到就累加 1 ,这样的好处就是规避了 覆盖次数 带来的不准确性。

「3」 共返回三个字段 total_lines (总行数)、covered_lines(覆盖行数)、coverage( 行覆盖率==covered_lines / total_lines )。

图片加载失败




calculate_branch_coverage

「1」 计算分支覆盖率的逻辑和行覆盖率大致一致,只是多了一个 identify_branches(),用正则判断是否为分支(if,switch等)。

「3」 也是返回三个字段 coverage( 分支覆盖率 == total_branches/ covered_branches )、 total_branches (总分支数)、covered_branches(覆盖分支数)、



calculate_files_branch_coverages

计算目录下所有文件的分支覆盖率

「1」 遍历 directory 下所有 txt 文件,对单个文件进行分支覆盖率的判断,判断逻辑和之前分析的计算分支覆盖率一致,计算结果采用字典处理。


1 条评论
某人
表情
可输入 255
用户U2fSWs6WBe
2025-05-06 11:39 0 回复
哇塞,是pwn帝,居然被我碰见了
😍
目录