By Artur Cygan
Fuzzing—one of the most successful techniques for finding security bugs, consistently featured in articles and industry conferences—has become so popular that you may think most important software has already been extensively fuzzed. But that’s not always the case. In this blog post, we show how we fuzzed the ZBar barcode scanning library and why, despite our limited time budget, we found serious bugs: an out-of-bounds stack buffer write that can lead to arbitrary code execution with a malicious barcode, and a memory leak that can be used to perform a denial-of-service attack.
ZBar is an open-source library for reading barcodes written in C. It supports an impressive number of barcode formats, including QR codes. One of our clients used it, so we wanted to quickly assess its security. Given the extensive amount of code, manual review was not an option. Since we noticed no public mention of fuzzing, we decided to give it a shot.
You might ask: how do you know whether or not software has been fuzzed? Although there’s no definitive answer to this question, it’s possible to make some educated guesses. First, we can check the repository for any mention of fuzzing, including searching issues, pull requests, and the code itself. For instance, this issue proposes a fuzzing harness, but it was likely never run. Second, we can check the oss-fuzz projects. If the project is fuzzed with oss-fuzz, it’s worth checking if the fuzzing harnesses are targeting the functionality we’re interested in and whether the project actually works. We observed cases where project builds were failing for months and were not actively fuzzed. Similarly to the project’s repository, oss-fuzz issues and pull requests can contain interesting information. Developers expressed some interest in bringing ZBar to oss-fuzz, but this was ultimately abandoned.
By this point we knew two things about ZBar: it was barely fuzzed (or not fuzzed at all), and we identified starting points for creating our own fuzzing campaign.
To fuzz ZBar, it has to be built with sanitizer and fuzzer instrumentation. Building an unfamiliar project can be a time-consuming challenge on its own, and adding instrumentation for fuzzing often makes this task even more difficult. For that reason, it’s useful to take an existing build and tweak it. Fortunately, ZBar is already packaged in Nixpkgs, so we could quickly modify the build:
zbar-instrumented = with pkgs; (zbar.override { stdenv = clang16Stdenv; }).overrideAttrs (orig: { buildInputs = orig.buildInputs ++ [ llvmPackages_16.openmp ]; dontStrip = true; doCheck = false; # tests started failing with sanitizers CFLAGS = "-g -fsanitize=address,fuzzer-no-link"; LDFLAGS = "-g -fsanitize=address,fuzzer-no-link"; });
Figure 1: Instrumenting ZBar for fuzzing
Nix packages are described with the Nix programming language and can be easily manipulated in various ways. In the case above, we use override to modify inputs defined by the package where we set the package’s compiler to Clang (otherwise, GCC is used by default). The following overrideAttrs
function is a free-form override that allows us to modify anything we want. With overrideAttrs
, we add the missing openmp
dependency, disable stripping so that debug build works properly, and disable the tests. Finally, we add the instrumentation compiler and linker flags for AddressSanitizer and libFuzzer. (If you’re unfamiliar with the instrumentation flags, our AppSec Testing Handbook has excellent guidance.)
Obviously, Nix is not the only answer to this problem. Depending on the software and packaging, tweaking existing packages might be more difficult. However, we highly recommend trying it out, as we found it to be often the quickest way to achieve the goal.
After preparing the instrumentation, we need to identify the fuzzing target. This part heavily depends on the project and can be non-trivial. Luckily, in ZBar the target was quite obvious: the function that takes an image and decodes barcode data from it. At this point there are a few questions to answer. How big should the image be? By default, ZBar tries to read all the known code types. Should we configure the scanner to specific codes or just try them all at once? We think it’s important not to overthink this and start with something to see how it performs. We started with the following harness, based on the official example:
#include #include #include 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); /* create a reader */ zbar_image_scanner_t *scanner = zbar_image_scanner_create(); /* configure the reader */ zbar_image_scanner_set_config(scanner, (zbar_symbol_type_t)0, ZBAR_CFG_ENABLE, 1); zbar_scan_image(scanner, image); /* clean up */ zbar_image_destroy(image); zbar_image_scanner_destroy(scanner); return 0; }
Figure 2: Initial testing harness
In this harness, we essentially modified the sample to take the input image from the fuzzer and locked it down to a 2-by-2 pixel square (8 bits per pixel). Running this harness resulted in one LeakSanitizer
crash reporting a memory leak. Because libFuzzer
stops at the first crash, we disabled the memory leak detection with -detect_leaks=0
and continued fuzzing. After a while, the coverage gains appeared to stall, so we decided to enlarge the input image to 4-by-4 pixels. Surprisingly, libFuzzer struggled to figure out that input should be of size 1024 and couldn’t start fuzzing. Even tweaking the max_len
and len_control
options didn’t help. we managed to kickstart fuzzing by manually passing a seed input of the right size:
$ head -c 1024 /dev/zero > seed $ ./result/bin/zbar-fuzz -detect_leaks=0 -seed_inputs=seed
Figure 3: Manually passing the seed input
After this, the fuzzer was able to quickly find another crash from AddressSanitizer caused by a stack buffer overflow. If you paid attention to the ZBar instrumentation code, we mentioned in the comment that its tests are disabled due to sanitizer failure. It turned out the failure during tests wasn’t a false positive and concerned the same bug the fuzzer discovered.
Even with this simple approach, we managed to find some bugs in the library. However, with more time, we could have made a number of improvements to find even more bugs:
It turned out that the stack buffer out-of-bounds write bug was independently reported around the same time by another researcher. The vulnerability was assigned CVE-2023-40890 and was fixed in commit 012a030. The issue lied in the lookup_sequence function, as the fuzzer pointed out:
==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)
Figure 4: Fuzzer triggered of out-of-bounds write bug
This memory leak bug opens a denial-of-service attack vector, especially since the leak size depends on the input and appears to be the image border size / 2 * 8 * 3 bytes
, so for an image with a border of 512, the leak is 6KiB. A program using ZBar to repeatedly scan untrusted codes can eventually exhaust memory and crash. The root issue is in the _zbar_sq_decode
function, which fails to free allocated memory under certain error conditions. This is again correctly pointed out by the fuzzer:
==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).
Figure 5: Fuzzer triggers a memory leak bug
The root cause of the leak is missing memory cleanup in error paths. There are two instances where the _zbar_sq_decode
function returns without executing the cleanup code under the free_borders
label.
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;
Figure 6: _zbar_sq_decode returns without executing cleanup code
We reported this issue along with the patch to the maintainer, however, after an extended period of time we still haven’t heard back. We published this patch on our ZBar fork and opened a pull request in the upstream ZBar repository.
To reproduce the research from this article, save the fuzzing harness shown earlier as zbar_harness.cpp
and the following Nix file as zbar-fuzz.nix
. The Nix file already contains the instrumented ZBar build and the harness build. Build it with nix-build zbar-fuzz.nix
and run ./result/bin/zbar-fuzz
. The postInstall
phase is not strictly required but ensures that the harness has llvm-symbolizer available to show the source locations, which helps in diagnosing the root cause.
let # nixpkgs snapshot from Aug 7, 2023 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; # tests fail with sanitizer 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 ''; }
Figure 7: Instrumented ZBar build and the harness build
There are a few takeaways from this experiment. First, it’s important to fuzz the unsafe code even if you don’t have a lot of time to do so. Other researchers can expand on the work by increasing the code coverage of the fuzzer.
Cut out any unnecessary features to limit attack vectors. ZBar by default scans all code types, which means that an attacker can trigger a bug in any of the scanners. If you only need to scan QR codes for instance, then ZBar can be configured to do so in the code:
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);
Figure 8: Configuring ZBar to scan only QR codes
Or when using the zbarimg
CLI program, add the options: --set '*.enable=0' --set 'qr.enable=1'
.
Finally, add sanitizer instrumentation to your build. At the bare minimum, you should use AddressSanitizer. As this ZBar example shows, if the test were built with sanitizers, it would have caught a critical memory safety vulnerability. Another benefit is that sanitizers save time and effort for adding fuzzing to a project, as sanitizers are essentially a required step for fuzzing C/C++ code.
We use fuzzing extensively at Trail of Bits. Take a look at our Testing Handbook for more resources, and contact us if you’re interested in custom fuzzing for your project.
*** This is a Security Bloggers Network syndicated blog from Trail of Bits Blog authored by Trail of Bits. Read the original post at: https://blog.trailofbits.com/2024/10/31/fuzzing-between-the-lines-in-popular-barcode-software/