流行条码软件中的模糊测试实践 - Trail of Bits博客
评估项目的模糊测试状态
模糊测试作为发现安全漏洞最有效的技术之一,虽然频繁出现在行业会议和文章中,但并不意味着所有重要软件都经过了充分测试。本文展示了如何对ZBar条码扫描库进行模糊测试,并在有限时间内发现了严重漏洞:一个可通过恶意条码导致任意代码执行的栈缓冲区越界写入漏洞,以及一个可用于拒绝服务攻击的内存泄漏漏洞。
ZBar是一个用C语言编写的开源条码读取库,支持包括QR码在内的多种条码格式。由于客户使用该库,我们需要快速评估其安全性。面对大量代码,手动审计不可行。在发现没有公开的模糊测试记录后,我们决定尝试模糊测试。
至此我们了解到ZBar几乎未被模糊测试(或完全未测试),并确定了创建自己测试活动的起点。
构建插桩
要对ZBar进行模糊测试,必须使用消毒剂和模糊测试器插桩进行构建。构建不熟悉的项目本身就很耗时,添加模糊测试插桩会使任务更加困难。因此,最好使用现有构建并进行调整。幸运的是,ZBar已打包在Nixpkgs中,我们可以快速修改构建:
1
2
3
4
5
6
7
8
9
|
zbar-instrumented = with pkgs; (zbar.override {
stdenv = clang16Stdenv;
}).overrideAttrs (orig: {
buildInputs = orig.buildInputs ++ [ llvmPackages_16.openmp ];
dontStrip = true;
doCheck = false; # 测试在使用消毒剂时开始失败
CFLAGS = "-g -fsanitize=address,fuzzer-no-link";
LDFLAGS = "-g -fsanitize=address,fuzzer-no-link";
});
|
图1:为模糊测试插桩ZBar
Nix包使用Nix编程语言描述,可以通过各种方式轻松操作。在上面的例子中,我们使用override修改包定义的输入,将编译器设置为Clang(默认使用GCC)。overrideAttrs函数是一个自由形式的覆盖,允许修改任何内容。通过overrideAttrs,我们添加了缺失的openmp依赖,禁用剥离以确保调试构建正常工作,并禁用测试。最后,我们为AddressSanitizer和libFuzzer添加了插桩编译器和链接器标志。
显然,Nix不是唯一解决方案。根据软件和打包方式,调整现有包可能更困难。但我们强烈建议尝试,因为我们发现这通常是最快实现目标的方法。
如何识别目标
准备插桩后,需要识别模糊测试目标。这部分高度依赖项目,可能很复杂。幸运的是,在ZBar中目标非常明显:接收图像并从中解码条码数据的函数。此时需要回答几个问题:图像应该多大?默认情况下,ZBar尝试读取所有已知代码类型。应该将扫描器配置为特定代码还是一次尝试所有?我们认为不要过度思考,先开始测试看看性能如何。我们基于官方示例开始了以下工具:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
#include <stdio.h>
#include <stdlib.h>
#include <zbar.h>
using namespace zbar;
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, uint32_t size) {
int width = 16, height = 16;
if (size != width*height) return 1;
zbar_image_t *image = zbar_image_create();
if(!image)
return 0;
zbar_image_set_size(image, width, height);
zbar_image_set_format(image, zbar_fourcc('Y', '8', '0', '0'));
zbar_image_set_data(image, data, size, NULL);
/* 创建阅读器 */
zbar_image_scanner_t *scanner = zbar_image_scanner_create();
/* 配置阅读器 */
zbar_image_scanner_set_config(scanner, (zbar_symbol_type_t)0, ZBAR_CFG_ENABLE, 1);
zbar_scan_image(scanner, image);
/* 清理 */
zbar_image_destroy(image);
zbar_image_scanner_destroy(scanner);
return 0;
}
|
图2:初始测试工具
在这个工具中,我们基本上修改了示例以从模糊测试器获取输入图像,并将其锁定为16x16像素正方形(每像素8位)。运行此工具导致一个LeakSanitizer崩溃报告内存泄漏。由于libFuzzer在第一次崩溃时停止,我们使用-detect_leaks=0
禁用内存泄漏检测并继续模糊测试。一段时间后,覆盖率增益似乎停滞,因此我们决定将输入图像扩大到32x32像素。令人惊讶的是,libFuzzer难以确定输入大小应为1024,无法开始模糊测试。即使调整max_len和len_control选项也没有帮助。我们通过手动传递正确大小的种子输入成功启动了模糊测试:
1
2
|
head -c 1024 /dev/zero > seed
./result/bin/zbar-fuzz -detect_leaks=0 -seed_inputs=seed
|
图3:手动传递种子输入
此后,模糊测试器能够快速找到另一个由栈缓冲区溢出引起的AddressSanitizer崩溃。如果您注意了ZBar插桩代码,我们在注释中提到其测试因消毒剂失败而禁用。结果证明测试期间的失败不是误报,与模糊测试器发现的错误相同。
即使使用这种简单方法,我们也在库中发现了一些错误。然而,如果有更多时间,我们可以进行多项改进以发现更多错误:
- 使用代码类型图片初始化语料库,帮助模糊测试器更快覆盖代码
- 针对特定代码,帮助模糊测试器维护同质语料库并生成更准确的突变
- 检查停滞时的代码覆盖率,帮助模糊测试器通过任何困难分支
诊断崩溃
结果发现,栈缓冲区越界写入错误大约在同一时间被另一位研究人员独立报告。该漏洞被分配为CVE-2023-40890,并在提交012a030中修复。问题在于lookup_sequence函数,正如模糊测试器指出的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
==22005==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fa297900578 at pc 0x7fa299b84ee2 bp 0x7ffe86531ef0 sp 0x7ffe86531ee8
WRITE of size 4 at 0x7fa297900578 thread T0
#0 0x7fa299b84ee1 in lookup_sequence /tmp/nix-build-zbar-0.23.92.drv-0/source/zbar/decoder/databar.c:698:12
#1 0x7fa299b84ee1 in match_segment_exp /tmp/nix-build-zbar-0.23.92.drv-0/source/zbar/decoder/databar.c:758:21
#2 0x7fa299b7fc02 in decode_char /tmp/nix-build-zbar-0.23.92.drv-0/source/zbar/decoder/databar.c:1081:16
#3 0x7fa299b7e225 in _zbar_decode_databar /tmp/nix-build-zbar-0.23.92.drv-0/source/zbar/decoder/databar.c:1269:11
#4 0x7fa299b756a6 in zbar_decode_width /tmp/nix-build-zbar-0.23.92.drv-0/source/zbar/decoder.c:274:15
#5 0x7fa299b726c1 in process_edge /tmp/nix-build-zbar-0.23.92.drv-0/source/zbar/scanner.c:173:16
#6 0x7fa299b726c1 in zbar_scanner_flush /tmp/nix-build-zbar-0.23.92.drv-0/source/zbar/scanner.c:186:35
#7 0x7fa299b7088a in quiet_border /tmp/nix-build-zbar-0.23.92.drv-0/source/zbar/img_scanner.c:708:5
#8 0x7fa299b7088a in _zbar_scan_image /tmp/nix-build-zbar-0.23.92.drv-0/source/zbar/img_scanner.c:1020:13
#9 0x7fa299b6e978 in zbar_scan_image /tmp/nix-build-zbar-0.23.92.drv-0/source/zbar/img_scanner.c:1146:12
#10 0x55c5b5f36a0f in LLVMFuzzerTestOneInput /tmp/nix-build-zbar-fuzz-0.23.92.drv-0/zbar/fuzz.cpp:25:3
...
#17 0x55c5b5d192e4 in _start (/nix/store/1lk9b8j92dx5xjfnhwh2g3x2g4d9mvsd-zbar-fuzz-0.23.92/bin/.zbar-fuzz-wrapped+0x352e4)
Address 0x7fa297900578 is located in stack of thread T0 at offset 376 in frame
#0 0x7fa299b80b8f in match_segment_exp /tmp/nix-build-zbar-0.23.92.drv-0/source/zbar/decoder/databar.c:709
This frame has 4 object(s):
[32, 120) 'bestsegs' (line 711)
[160, 248) 'segs' (line 711)
[288, 376) 'seq' (line 711) <== Memory access at offset 376 overflows this variable
[416, 544) 'iseg' (line 713)
|
图4:模糊测试器触发的越界写入错误
这个内存泄漏漏洞打开了拒绝服务攻击向量,特别是因为泄漏大小取决于输入,似乎是图像边框大小/283字节,因此对于边框为512的图像,泄漏为6KiB。使用ZBar重复扫描不可信代码的程序最终可能耗尽内存并崩溃。根本问题在于_zbar_sq_decode函数在某些错误条件下未能释放分配的内存。这再次被模糊测试器正确指出:
1
2
3
4
5
6
7
8
9
10
11
12
|
==21815==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 48 byte(s) in 1 object(s) allocated from:
#0 0x55df498b66ff in __interceptor_malloc (/nix/store/ncb5qgjr6jds4na1iadf5cxgdym6fbl5-zbar-fuzz-0.23.92/bin/.zbar-fuzz-wrapped+0x20b6ff)
#1 0x7f71e9334cbf in _zbar_sq_decode /tmp/nix-build-zbar-0.23.92.drv-0/source/zbar/sqcode.c:397:19
#2 0x7f71e92d7cf8 in _zbar_scan_image /tmp/nix-build-zbar-0.23.92.drv-0/source/zbar/img_scanner.c:1055:5
#3 0x7f71e92d5978 in zbar_scan_image /tmp/nix-build-zbar-0.23.92.drv-0/source/zbar/img_scanner.c:1146:12
#4 0x55df498fda0f in LLVMFuzzerTestOneInput /tmp/nix-build-zbar-fuzz-0.23.92.drv-0/zbar/fuzz.cpp:25:3
...
#11 0x7f71e8f8bacd in __libc_start_call_main (/nix/store/46m4xx889wlhsdj72j38fnlyyvvvvbyb-glibc-2.37-8/lib/libc.so.6+0x23acd) (BuildId: 2ed90a3fa8dfeee1e77c301df6ba346580b73e8a)
...
SUMMARY: AddressSanitizer: 144 byte(s) leaked in 3 allocation(s).
|
图5:模糊测试器触发的内存泄漏错误
泄漏的根本原因是错误路径中缺少内存清理。有两个实例_zbar_sq_decode函数返回而未执行free_borders标签下的清理代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
diff --git a/zbar/sqcode.c b/zbar/sqcode.c
index 422c803d..a5e808fc 100644
--- a/zbar/sqcode.c
+++ b/zbar/sqcode.c
@@ -371,7 +371,7 @@ found_start:;
border_len = 1;
top_border = malloc(sizeof(sq_point));
if (!top_border)
- return 1;
+ goto free_borders;
top_border[0] = top_left_dot.center;
}
}
@@ -471,7 +471,7 @@ found_start:;
}
}
if (cur_len != border_len || border_len < 6)
- return 1;
+ goto free_borders;
inc_x = right_border[5].x - right_border[3].x;
inc_y = right_border[5].y - right_border[3].y;
right_border[2].x = right_border[3].x - 0.5 * inc_x;
|
图6:_zbar_sq_decode返回而未执行清理代码
我们向维护者报告了此问题及补丁,但经过较长时间仍未收到回复。我们在ZBar分叉上发布了此补丁,并在上游ZBar存储库中打开了拉取请求。
整合所有内容
要复现本文的研究,请将前面显示的模糊测试工具保存为zbar_harness.cpp,并将以下Nix文件保存为zbar-fuzz.nix。Nix文件已包含插桩的ZBar构建和工具构建。使用nix-build zbar-fuzz.nix
构建,并运行./result/bin/zbar-fuzz
。postInstall阶段不是严格必需的,但确保工具具有llvm-symbolizer可用以显示源位置,这有助于诊断根本原因。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
let
# 2023年8月7日的nixpkgs快照
pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/011567f35433879aae5024fc6ec53f2a0568a6c4.tar.gz") {};
zbar-instrumented = with pkgs; (zbar.override {
stdenv = clang16Stdenv;
}).overrideAttrs (orig: {
buildInputs = orig.buildInputs ++ [ llvmPackages_16.openmp ];
dontStrip = true;
doCheck = false; # 测试在使用消毒剂时失败
CFLAGS = "-g -fsanitize=address,fuzzer-no-link";
LDFLAGS = "-g -fsanitize=address,fuzzer-no-link";
});
in with pkgs; clang16Stdenv.mkDerivation rec {
pname = "zbar-fuzz";
version = zbar.version;
src = ./.;
nativeBuildInputs = [ makeWrapper ];
buildInputs = [ zbar-instrumented ];
dontStrip = true;
buildPhase = ''
mkdir -p $out/bin
clang++ zbar_harness.cpp -fsanitize=address,fuzzer -g -lzbar -o $out/bin/zbar-fuzz
'';
postInstall = ''
wrapProgram $out/bin/zbar-fuzz \
--prefix PATH : ${lib.getBin llvmPackages_16.llvm}/bin
'';
}
|
图7:插桩的ZBar构建和工具构建
经验教训
从这个实验中有几个收获。首先,即使没有太多时间,对不安全代码进行模糊测试也很重要。其他研究人员可以通过增加模糊测试器的代码覆盖率来扩展这项工作。
削减任何不必要的功能以限制攻击向量。ZBar默认扫描所有代码类型,这意味着攻击者可以触发任何扫描器中的错误。如果只需要扫描QR码,可以在代码中配置ZBar:
1
2
|
zbar_image_scanner_set_config(scanner, (zbar_symbol_type_t)0, ZBAR_CFG_ENABLE, 0);
zbar_image_scanner_set_config(scanner, ZBAR_QRCODE, ZBAR_CFG_ENABLE, 1);
|
图8:配置ZBar仅扫描QR码
或者在使用zbarimg CLI程序时,添加选项:--set '*.enable=0' --set 'qr.enable=1'
。
最后,向构建中添加消毒剂插桩。至少应使用AddressSanitizer。如这个ZBar示例所示,如果测试使用消毒剂构建,本可以发现关键的内存安全漏洞。另一个好处是消毒剂节省了向项目添加模糊测试的时间和精力,因为消毒剂本质上是模糊测试C/C++代码的必要步骤。
我们在Trail of Bits广泛使用模糊测试。查看我们的测试手册获取更多资源,如果您对项目的自定义模糊测试感兴趣,请联系我们。