Фаззинг — одна из самых успешных методик для поиска багов безопасности, о нём постоянно говорят в статьях и на отраслевых конференциях. Он стал настолько популярным, что большинство важного ПО, казалось бы, должно подвергаться тщательному фаззингу. Но это не всегда так. В этом посте мы покажем, как фаззили библиотеку сканирования штрих-кодов ZBar, и почему, несмотря на ограниченность по времени, обнаружили в ней серьёзные баги: запись в буфер стека out-of-bounds, которая может привести к произвольному выполнению кода при помощи зловредного штрих-кода, и утечку памяти, которую можно использовать для выполнения атаки «отказ в обслуживании» (denial-of-service).

ZBar — это опенсорсная библиотека для считывания штрих-кодов, написанная на C. Она поддерживает впечатляющий набор форматов штрих-кодов, в том числе и QR-коды. Её использовал один из наших клиентов, поэтому мы решили вкратце проверить её безопасность. Учитывая огромный объём кода, вручную тестировать его мы не могли. Так как мы не нашли упоминаний о фаззинге этой библиотеки, то решили попробовать его.

▍ Оценка состояния фаззинга проекта


Вы можете задать вопрос: как мы узнали, что ПО не подвергалось фаззингу? Хотя чёткого ответа на него нет, можно сделать обоснованные предположения. Во-первых, можно проверить наличие упоминания фаззинга в репозитории, в том числе поискать по issue, пул-реквестам и в самом коде. Например, в этом issue предлагается обвязка для фаззинга, но, похоже, её так никогда и не запускали. Во-вторых, можно проверить проекты oss-fuzz. Если проект подвергался фаззингу при помощи oss-fuzz, то стоит проверить, нацелена ли была обвязка фаззинга на интересующую нас функциональность и работает ли проект вообще. Мы видели случаи, в которых сборки проектов завершались неудачами в течение нескольких месяцев и не подвергались активному фаззингу. Кроме репозитория проекта, интересная информация может содержаться в issue oss-fuzz и в пул-реквестах. Разработчики выразили некоторый интерес к проверке ZBar при помощи oss-fuzz, но в конечном итоге отказались от этой идеи.

На этом этапе мы знали о ZBar две вещи: то, что её едва проверяли фаззингом (или вообще не проверяли), а также возможные начальные точки для создания собственной кампании по фаззингу.

▍ Оснащаем сборку


Для фаззинга ZBar её нужно собрать с инструментацией санитайзера и фаззера. Сборка незнакомого проекта сама по себе может быть длительным занятием, а добавление инструментации для фаззинга часто делает эту задачу ещё более сложной. Поэтому полезно будет взять готовую сборку и модифицировать её. К счастью, ZBar уже упакована в Nixpkgs, так что мы можем быстро изменить сборку:

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";
});

Пакеты Nix описываются на языке программирования Nix, и ими можно легко манипулировать различным образом. В приведённом выше случае мы использовали переопределение, чтобы модифицировать определённые пакетом входные данные, заменив компилятор на Clang (в противном случае по умолчанию используется GCC). Последующая функция overrideAttrs — это произвольное переопределение, позволяющее нам изменить всё необходимое. При помощи overrideAttrs мы добавляем недостающую зависимость openmp, отключаем stripping, чтобы правильно работали отладочные сборки, и отключаем тесты. Далее мы добавляем флаги инструментации компилятора и компоновщика для AddressSanitizer и libFuzzer. (Если вы незнакомы с флагами инструментации, то в нашей AppSec Testing Handbook есть замечательное руководство по ним.)

Очевидно, Nix — не единственное решение этой проблемы. В некоторых случаях, зависящих от ПО и способов создания пакетов, настройка существующих пакетов может быть сложнее. Однако мы крайне рекомендуем попробовать его, потому что для нас он часто оказывался самым быстрым способом достижения целей.

▍ Как выявить цель для фаззинга


Подготовив инструментацию, мы должны выявить цель для фаззинга. Это сильно зависит от проекта и может быть нетривиальной задачей. К счастью, в ZBar цель достаточно очевидна: функция, получающая изображение и декодирующая из него данные. На этом этапе нам нужно ответить на несколько вопросов. Насколько большим должно быть изображение? По умолчанию ZBar пытается считывать все известные типы кодов. Нужно ли нам сконфигурировать сканер на конкретные коды или просто пробовать их все одновременно? Мы считаем, что здесь не нужно слишком углубляться и просто попробовать что-нибудь, чтобы проверить, как ведёт себя библиотека. Мы начали со следующей обвязки, созданной на основе официального примера:

#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 на 2 пикселя (8 бита на пиксель). Запуск этой обвязки привёл к одному вылету LeakSanitizer с сообщением об утечке памяти. Так как libFuzzer останавливается на первом вылете, мы отключили распознавание утечек памяти при помощи -detect_leaks=0 и продолжили фаззинг. Спустя какое-то время рост покрытия прекратился, поэтому мы решили увеличить входное изображение до размера 4x4. К нашему удивлению, у libFuzzer возникли сложности с определением того, что входные данные должны иметь размер 1024, и он не мог начать фаззинг. Не помогло даже изменение max_len и len_control. Мы смогли запустить фаззинг вручную, передав порождающие входные данные нужного размера:

$ head -c 1024 /dev/zero > seed
$ ./result/bin/zbar-fuzz -detect_leaks=0 -seed_inputs=seed

После этого фаззер смог быстро найти другой вылет AddressSanitizer, вызванный переполнением буфера стека. Если вы обратили внимание на код инструментации ZBar, то заметили комментарий, что тесты отключены из-за сбоя санитайзера. Оказалось, что сбой во время тестов не был ложноположительным и касался того же бага, что и обнаруженный фаззером.

Даже при такой простой методике мы смогли найти в библиотеке несколько багов. Однако, если бы у нас было больше времени, мы бы могли внести множество улучшений, чтобы найти ещё больше багов:

  • Инициировать корпус с изображениями типов кодов, чтобы помочь фаззеру быстрее выполнять покрытие кода.
  • Выбирать в качестве целей конкретные коды, что поможет фаззеру поддерживать однородный корпус и генерировать более точные изменения данных.
  • Проверять покрытие кода там, где оно не увеличивается, чтобы помочь фаззеру проходить сложные ветвления.

▍ Диагностируем вылеты


Оказалось, что о баге записи в буфер стека out-of-bounds независимо от нас сообщил ещё один исследователь. Уязвимости был присвоен CVE-2023-40890, и её устранили в коммите 012a030. Как указал фаззер, проблема скрывалась в функции lookup_sequence:

==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)

Баг утечки памяти открывал вектор denial-of-service, в частности, потому, что размер утечки зависел от входных данных; похоже, что он равен размеру границы изображения / 2 * 8 * 3 байта, так что для изображения с границей в 512 утечка составляет 6 КиБ. Программа, многократно использующая ZBar для сканирования ненадёжных кодов, в конечном итоге исчерпает память и вылетит. Первопричина находилась в функции _zbar_sq_decode, которой при определённых условиях ошибки не удаётся освободить выделенную память. На это тоже правильно указал фаззер:

==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).

Первопричиной утечки стало отсутствие очистки памяти на путях возникновения ошибки. В двух случаях функция _zbar_sq_decode выполняла возврат без выполнения кода очистки под меткой free_borders.

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;

Мы сообщили об этой проблеме мейнтейнеру и выслали патч, однако долгое время не получали никакой реакции. Мы опубликовали этот патч в своём форке ZBar и открыли пул-реквест в upstream-репозиторий ZBar.

▍ Объединяем всё вместе


Чтобы воспроизвести описанное в посте исследование, сохраните показанную выше обвязку фаззинга как zbar_harness.cpp, а представленный ниже файл Nix как zbar-fuzz.nix. Файл Nix уже содержит инструментированную сборку ZBar и сборку с обвязкой. Собрать его можно при помощи nix-build zbar-fuzz.nix, после чего выполнить ./result/bin/zbar-fuzz. Фаза postInstall необязательна, но гарантирует, что обвязка будет иметь llvm-symbolizer для отображения местоположения исходников, что помогает в диагностировании первопричин.

let
  # снэпшот nixpkgs за 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; # с санитайзером тесты вылетают
    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
 '';
}

▍ Выводы


Из этого эксперимента можно сделать несколько выводов. Во-первых, важно проводить фаззинг небезопасного кода, даже если у вас особо нет на это времени. Другие исследователи могут продолжить вашу работу, расширив покрытие кода фаззером.

Устраняйте ненужные фичи, чтобы ограничить векторы атаки. По умолчанию ZBar сканирует все типы штрих-кодов, то есть нападающий может вызвать баг в любом из сканеров. Допустим, если вам нужно сканировать только QR-коды, то для этого ZBar можно настроить в коде:

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);

Или при использовании CLI-программы zbarimg можно добавить опции --set '*.enable=0' --set 'qr.enable=1'.

Наконец, добавьте в свою сборку инструментацию санитайзеров. Как минимум следует использовать AddressSanitizer. Как видно из этого примера с ZBar, если бы тесты собирались с санитайзерами, то они бы обнаружили критическую уязвимость безопасности памяти. Ещё одно преимущество заключается в том, что санитайзеры экономят время и усилия по добавлению фаззинга в проект, поскольку санитайзеры — это, по сути, обязательный этап для фаззинга кода на C/C++.

Telegram-канал со скидками, розыгрышами призов и новостями IT ?

Комментарии (0)