流行条码软件中的模糊测试技术
Artur Cygan
2024年10月31日
应用安全,模糊测试
评估项目的模糊测试状态
你可能会问:如何知道软件是否经过模糊测试?虽然没有确定答案,但可以进行有根据的猜测。首先,我们可以检查代码库中是否有模糊测试的提及,包括搜索问题、拉取请求和代码本身。例如,这个问题提出了一个模糊测试工具,但可能从未运行过。其次,我们可以检查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
|
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_bord
|