OpenSSL 1.1 Fuzzing 实战指南:用 LibFuzzer 与 AFL 对 SRS 内置 OpenSSL 进行健壮性测试
【免费下载链接】srsSRS is a simple, high-performance, AI-driven real-time media server supporting RTMP, WebRTC, HLS, HTTP-FLV, HTTP-TS, SRT, MPEG-DASH, and GB28181, with codec support for H.264, H.265, AV1, VP9, AAC, Opus, and G.711.项目地址: https://gitcode.com/GitHub_Trending/sr/srs
SRS 媒体服务器在其 trunk/3rdparty/openssl-1.1-fit 目录中内置了裁剪过的 OpenSSL 1.1 源码,用于支撑 HTTPS、WebRTC/DTLS 等加密链路。本文以该内置 OpenSSL 源码树中的 fuzz/README.md 为核心,完整讲解如何用 LLVM LibFuzzer 与 AFL 对 OpenSSL 的 TLS 握手、ASN.1 解析、大整数运算等高风险入口做模糊测试,读完你将掌握从编译环境搭建、Fuzzer 构建、语料(corpus)管理到崩溃复现的完整工作流。
为什么需要 Fuzz OpenSSL
密码学库是安全攻防的第一线:TLS 握手解析、ASN.1/DER 编码、X.509 证书解析、BIGNUM 大整数运算都是历史上漏洞高发的区域。OpenSSL 官方正是通过在源码树内提供fuzz/目录,把模糊测试作为日常开发的一部分。在本仓库的 SRS 场景下,OpenSSL 被用于 RTMP over HTTPS、WebRTC DTLS 等实时媒体链路,一旦解析代码存在内存破坏类缺陷,后果会直接波及媒体服务的可用性与安全性。
OpenSSL 1.1 的 fuzz 体系同时支持两条主流工具链:
| 工具链 | 特点 | 触发配置开关 |
|---|---|---|
| LibFuzzer(clang 自带) | 进程内覆盖率引导,速度极快 | enable-fuzz-libfuzzer |
| AFL(afl-clang-fast) | 独立进程 + 持久模式,生态成熟 | enable-fuzz-afl |
两种方式共享同一套fuzz/*.c模糊测试目标与语料目录,只是驱动与编译插桩不同。下面从零开始依次演示。
准备 LibFuzzer 编译环境
LibFuzzer 依赖较新的 clang。官方文档给出的路径是从零搭建:
- 安装 git 并克隆 Chromium 维护的 clang 构建脚本,执行更新脚本拉取预编译的 clang:
$ sudo apt-get install git $ mkdir git-work $ git clone https://chromium.googlesource.com/chromium/src/tools/clang $ clang/scripts/update.py提示:可以定期
git pull并重跑update.py以保持 clang 版本较新,老版本可能无法正常工作。
- 把编译好的 clang 加入
PATH:
$ PATH=~/third_party/llvm-build/Release+Asserts/bin/:$PATH- 获取并编译 LibFuzzer 运行时库(以 svn 方式拉取 compiler-rt 中的 fuzzer 源码,然后手工编译成静态库):
$ cd $ sudo apt-get install subversion $ mkdir svn-work $ cd svn-work $ svn co https://llvm.org/svn/llvm-project/compiler-rt/trunk/lib/fuzzer Fuzzer $ cd Fuzzer $ clang++ -c -g -O2 -std=c++11 *.cpp $ ar r libFuzzer.a *.o $ ranlib libFuzzer.a这一步得到的libFuzzer.a将在下一步配置 OpenSSL 时通过--with-fuzzer-lib传入。
配置并用 LibFuzzer 构建 OpenSSL
进入内置的 OpenSSL 源码树,用 clang 作为编译器,开启 fuzz 模式进行配置:
$ CC=clang ./config enable-fuzz-libfuzzer \ --with-fuzzer-include=../../svn-work/Fuzzer \ --with-fuzzer-lib=../../svn-work/Fuzzer/libFuzzer.a \ -DPEDANTIC enable-asan enable-ubsan no-shared \ -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION \ -fsanitize-coverage=trace-pc-guard,indirect-calls,trace-cmp \ enable-ec_nistp_64_gcc_128 -fno-sanitize=alignment enable-tls1_3 \ enable-weak-ssl-ciphers enable-rc5 enable-md2 \ enable-ssl3 enable-ssl3-method enable-nextprotoneg \ --debug逐项拆解这一长串参数的作用:
| 参数 | 作用 |
|---|---|
enable-fuzz-libfuzzer | 启用 LibFuzzer 驱动(对应 Configure 中的 fuzz 配置逻辑) |
--with-fuzzer-include | 指向上面编译的 Fuzzer 头文件目录 |
--with-fuzzer-lib | 指向libFuzzer.a静态库 |
enable-asan enable-ubsan | 开启 AddressSanitizer 与 UndefinedBehaviorSanitizer,用于捕获内存越界、未定义行为 |
no-shared | 静态编译,避免动态库符号与插桩冲突 |
-DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION | 关键宏:让 client/server fuzzer 使用可预测的随机数,保证语料覆盖稳定(详见后文"随机数与可复现性") |
-fsanitize-coverage=trace-pc-guard,indirect-calls,trace-cmp | 覆盖率插桩类型,LibFuzzer 据此引导变异 |
enable-tls1_3 enable-weak-ssl-ciphers enable-rc5 enable-md2 enable-ssl3 enable-ssl3-method enable-nextprotoneg | 打开更多协议与弱密码族,扩大可 fuzz 的代码面 |
--debug | 开启调试符号与断言 |
接着编译,并指定链接器为clang++(因为 LibFuzzer 运行时是 C++):
$ sudo apt-get install make $ LDCMD=clang++ make -j启动 Fuzzer 并处理崩溃
编译完成后,fuzz/目录下会生成若干可执行文件(如asn1、x509、client、server、bignum、bndiv、cms、conf、crl、ct、asn1parse等),每个对应一个模糊测试目标。官方推荐通过辅助脚本启动:
$ fuzz/helper.py $FUZZER其中$FUZZER是fuzz/下的某个可执行文件名。以仓库中提供的 fuzz/helper.py 源码来看,它会自动完成三件事:
- 在
fuzz/corpora/$FUZZER/下创建(或复用)语料目录; - 创建
fuzz/corpora/$FUZZER-crash/崩溃目录; - 若存在
fuzz/corpora/$FUZZER-seed/种子目录则一并加入输入集合,最后以-artifact_prefix=$FUZZER-crash/参数启动 fuzzer。
因此一旦发生崩溃,对应的输入文件会自动落入:
fuzz/corpora/$FUZZER-crash/这个目录中的每个文件就是一条可复现崩溃的极小输入,可以直接用于后续回归测试与缺陷定位。
使用 AFL 进行模糊测试
如果你偏好 AFL 工作流,配置同样简洁:
$ sudo apt-get install afl-clang $ CC=afl-clang-fast ./config enable-fuzz-afl no-shared -DPEDANTIC \ enable-tls1_3 enable-weak-ssl-ciphers enable-rc5 enable-md2 \ enable-ssl3 enable-ssl3-method enable-nextprotoneg \ enable-ec_nistp_64_gcc_128 -fno-sanitize=alignment \ --debug $ make这里不再需要-fsanitize-coverage,插桩由afl-clang-fast完成;enable-fuzz-afl会让 fuzz/driver.c 编译为 AFL 专用入口:主循环调用__AFL_LOOP(10000)走持久化模式,从 stdin 读取不超过 64KB(BUF_SIZE 65536)的输入并喂给FuzzerTestOneInput,从而避免反复 fork 的开销。此外文档还提示以下 sanitizer 可按需叠加:
enable-asan, enable-ubsan, enable-msan运行某个 fuzzer 的命令为:
$ afl-fuzz -i fuzz/corpora/$FUZZER -o fuzz/corpora/$FUZZER/out fuzz/$FUZZER-i指定输入语料目录,-o指定输出目录(AFL 会把发现的新路径、崩溃、挂起分别放入out/queue、out/crashes、out/hangs)。
复现 fuzzer 发现的缺陷
当 fuzzer 产出崩溃文件后,需要用*-test二进制做最小化复现与调试。每个 fuzzer 目标都配套生成一个$FUZZER-test可执行程序,其主体编译自 fuzz/test-corpus.c:它把命令行给出的每个文件(也支持目录递归)逐条读入并调用FuzzerTestOneInput,任何崩溃都会如实暴露。
复现命令:
$ fuzz/$FUZZER-test $file需要注意的要点:
*-test二进制不需要用 fuzz 配置编译,不必设置CC、不必传enable-fuzz-*或-fsanitize-coverage;- 但部分选项仍然建议保留:例如
enable-asan/enable-ubsan能精准指示崩溃发生的时机与位置; - 对
client与server这两个 TLS fuzzer,复现时通常仍需要-DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION,以复现当初生成该输入时的随机数序列。
随机数与可复现性:FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
这是 OpenSSL fuzz 体系中最精妙的设计之一。TLS 握手过程中 client 和 server 会生成随机数参与密钥协商,这会导致一个副作用:语料覆盖随随机数变化而漂移,即使代码一行未改,每次运行 fuzzer 的覆盖曲线也在变,甚至整个测试套件的覆盖率统计都会随 commit 抖动。
为了让覆盖结果可复现、可比较,client与serverfuzzer 在定义了FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION时改用可预测的伪随机数。其实现就在 fuzz/rand.inc:
static int fuzz_bytes(unsigned char *buf, int num) { unsigned char val = 1; while (--num >= 0) *buf++ = val++; return 1; }它通过FuzzerSetRand()用RAND_set_rand_method()注册一个固定填充序列的RAND_METHOD,从而保证:client fuzzer 每次生成完全相同的 ClientHello,对应的 server 响应也可以离线预生成并固化进语料。同时文档强调:这个宏不会绕过任何哈希校验——语料中存放的是与固定随机数配套的正确哈希值,因此测试依然具备真实性。
另一个可复现性细节同样藏在源码里:client.c与server.c都重定义了time()函数,返回固定值FUZZTIME(1485898104),避免证书有效期、时间戳校验影响握手路径的可复现性(Windows 平台因链接机制差异不保证生效,仅会导致覆盖略有不同)。
覆盖率漂移与语料维护
正因为语料覆盖依赖 client/server 的默认行为(默认发送的 ClientHello 内容、默认的密码套件顺序等),一旦上游修改了默认行为,语料覆盖就会变化,此时必须同步更新语料。这是维护者需要持续跟进的工作。
官方在更新语料时,会针对多种配置分别生成 client/server 语料,再用 libfuzzer 的 merge 能力将各配置的增量覆盖合并到最小集合:
- 上文文档记录的默认配置组合;
- 去掉
enable-ec_nistp_64_gcc_128且不带--debug的配置; no-asm配置(关闭汇编实现,覆盖 C 代码路径);- 32 位构建;
- 默认配置外加生成 fuzzer 所需的必要选项。
这种"多配置生成 + merge 归并"的语料管理方式,可以理解为对同一组模糊目标做交叉验证,显著提升覆盖的完备性。
仓库中的各 Fuzzer 目标源码解析
fuzz/目录下的每个.c文件都实现统一的FuzzerInitialize/FuzzerTestOneInput/FuzzerCleanup接口(声明见 fuzz/fuzzer.h),按测试面可归为三类:
TLS 握手类:client 与 server
- fuzz/client.c:创建
SSLv23_method()上下文,设置ALL:eNULL:@SECLEVEL=0密码套件、SNI=localhost,把模糊输入直接写入内存 BIO 作为服务端发来的握手数据,驱动SSL_do_handshake,握手成功后继续SSL_read消费应用数据直到 EOF 或出错。 - fuzz/server.c:更复杂一些。它内嵌了内置的 RSA 证书/私钥、ECDSA 与 DSA 的 PEM 证书/私钥(以 DER/PEM 字节数组形式硬编码在源码中),加载进
SSL_CTX后把模糊输入作为客户端握手指向的数据。输入的最后一个字节被当作选项位:opt & 0x01时先尝试SSL_read_early_data处理 TLS 1.3 early data,再走完整握手。这使单个 fuzzer 能同时覆盖普通握手与 0-RTT 两条路径。
解析类:asn1、x509、cms、crl、conf、ct、asn1parse
- fuzz/asn1.c:维护一张巨大的
ASN1_ITEM表(涵盖X509、PKCS7/PKCS12、OCSP全系、RSA/DSA/EC参数、INT64/UINT64等数百种结构),对同一份输入逐一遍历,依次执行ASN1_item_d2i→ASN1_item_print→ASN1_item_i2d→ASN1_item_free的完整生命周期,并额外用d2i_*/i2d_*直接测试 DH、DSA、RSA、EC、SSL_SESSION 等类型的编解码。 - fuzz/x509.c:
d2i_X509解析后调用X509_print(会连带加载并打印公钥与扩展)、X509_issuer_and_serial_hash、再i2d_X509回写,覆盖证书解析、打印、哈希与重编码路径。 - 其余如
cms.c、crl.c、conf.c、ct.c、asn1parse.c分别针对 CMS 消息、CRL 吊销列表、OpenSSL 配置文件、证书透明度、ASN.1 通用解析器等入口,机制一致。
大整数运算类:bignum 与 bndiv
- fuzz/bignum.c:用输入前两个字节切分三段长度、第三个字节的低位决定符号,构造出三个大整数
b1、b2、b3,然后对比BN_mod_exp(优化实现)与BN_mod_exp_simple(朴素实现)的结果是否一致——这是一种差分测试:两个实现结果不一致即视为失败,用于捕获模幂运算的隐藏缺陷。 - fuzz/bndiv.c:对输入切分为被除数与除数,验证除法恒等式
b3*b2 + b4 == b1以及商/余数的符号规则(sign(d) == sign(a),0 <= r <= b),并限制输入上限 256KB 以避免超时。当被除数为 0 时校验商与余数均为 0。
三种驱动的协作关系小结
最后用一张逻辑图梳理 OpenSSL fuzz 体系的运行架构(不需要截图,理解即可):
模糊输入 ──▶ FuzzerTestOneInput(buf, len) │ ├─ LibFuzzer 模式(driver.c):LLVMFuzzerTestOneInput 包装,覆盖率引导 + 进程内变异 ├─ AFL 模式(driver.c):__AFL_LOOP 持久循环 + stdin 喂入 └─ test 模式(test-corpus.c):逐文件回放,用于复现 fuzz/corpora/$FUZZER-crash/- 同一份模糊目标代码(
fuzz/*.c)通过 fuzz/driver.c 中的条件编译适配三种运行方式:定义OPENSSL_NO_FUZZ_LIBFUZZER与否分别走 LibFuzzer 入口或 AFL 入口; - 语料与崩溃产物统一落在
fuzz/corpora/下,helper.py负责目录编排与参数注入; - 确定性由
FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION+ 固定随机数 + 固定时间共同保证,覆盖数据才具有跨 commit 的可比性。
对 SRS 这类把 OpenSSL 1.1 作为第三方依赖内置的媒体服务器项目而言,理解这套 fuzz 工作流的价值在于:你可以随时在内置的 fuzz/README.md 指导下,针对自己实际启用的协议组合(TLS、DTLS 相关配置)重新生成语料、运行 fuzzer,并将发现的崩溃回灌到*-test复现与修复,从而在把 OpenSSL 引入生产媒体服务之前,先把它"摩擦"得足够健壮。
【免费下载链接】srsSRS is a simple, high-performance, AI-driven real-time media server supporting RTMP, WebRTC, HLS, HTTP-FLV, HTTP-TS, SRT, MPEG-DASH, and GB28181, with codec support for H.264, H.265, AV1, VP9, AAC, Opus, and G.711.项目地址: https://gitcode.com/GitHub_Trending/sr/srs
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考