流行条码软件中的模糊测试 - The Trail of Bits博客
Artur Cygan
2024年10月31日
应用安全,模糊测试
页面内容
- 评估项目的模糊测试状态
- 构建插桩
- 如何识别目标
- 诊断崩溃
- 整合所有内容
- 经验教训
模糊测试——发现安全漏洞最成功的技术之一,经常出现在文章和行业会议中——已经变得如此流行,以至于你可能认为大多数重要软件都已经进行了广泛的模糊测试。但情况并非总是如此。在这篇博客文章中,我们展示了如何对ZBar条码扫描库进行模糊测试,以及为什么尽管时间有限,我们还是发现了严重漏洞:一个可能导致恶意条码任意代码执行的栈缓冲区越界写入,和一个可用于执行拒绝服务攻击的内存泄漏。
ZBar是一个用C语言编写的开源条码读取库。它支持令人印象深刻的条码格式数量,包括QR码。我们的一个客户使用了它,因此我们想快速评估其安全性。考虑到代码量巨大,手动审查不可行。由于我们没有发现公开提及模糊测试,我们决定尝试一下。
评估项目的模糊测试状态
你可能会问:如何知道软件是否已经进行了模糊测试?虽然这个问题没有明确的答案,但可以做出一些有根据的猜测。首先,我们可以检查存储库是否有任何提及模糊测试的内容,包括搜索问题、拉取请求和代码本身。例如,这个问题提出了一个模糊测试工具,但可能从未运行过。其次,我们可以检查oss-fuzz项目。如果项目使用oss-fuzz进行模糊测试,值得检查模糊测试工具是否针对我们感兴趣的功能,以及项目是否实际工作。我们观察到有些项目的构建失败数月,并且没有积极进行模糊测试。与项目存储库类似,oss-fuzz的问题和拉取请求可能包含有趣的信息。开发人员对将ZBar引入oss-fuzz表示了一些兴趣,但最终放弃了。
此时,我们对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添加了插桩编译器和链接器标志。如果你不熟悉插桩标志,我们的AppSec测试手册有很好的指导。
显然,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:模糊测试器触发的越界写入错误
这个内存泄漏错误打开了拒绝服务攻击向量,特别是因为泄漏大小取决于输入,似乎是图像边框大小/2 * 8 * 3字节,因此对于边框为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