From d4aa6d433baef6056299a2d7f696d4259654bb3d Mon Sep 17 00:00:00 2001 From: Nikolay Shaplov Date: Sun, 13 Apr 2025 10:44:29 +0300 Subject: [PATCH] =?utf8?q?=D0=9E=D0=B1=D1=89=D0=B8=D0=B9=20README=20=D0=B8?= =?utf8?q?=20=D1=81=D1=82=D0=B0=D1=80=D0=B0=D1=8F=20=D1=81=D1=82=D0=B0?= =?utf8?q?=D1=82=D1=8C=D1=8F=20=D0=BF=D1=80=D0=BE=20=D1=84=D0=B0=D0=B7?= =?utf8?q?=D0=B7=D0=B8=D0=BD=D0=B3=20=D1=81=D0=BE=20=D1=81=D0=BD=D0=B5?= =?utf8?q?=D0=BF=D1=88=D0=BE=D1=82=D0=B0=D0=BC=D0=B8=20=D1=84=D0=B0=D0=B9?= =?utf8?q?=D0=BB=D0=BE=D0=B2=D0=BE=D0=B9=20=D1=81=D0=B8=D1=81=D1=82=D0=B5?= =?utf8?q?=D0=BC=D1=8B=20=D0=BA=D0=BE=D1=82=D0=BE=D1=80=D1=83=D1=8E=20?= =?utf8?q?=D1=8F=20=D1=80=D0=B0=D0=BD=D1=8C=D1=88=D0=B5=20=D0=B2=20=D0=BB?= =?utf8?q?=D0=B8=D1=87=D0=BD=D0=BE=D0=BC=20plume=20=D0=BF=D1=83=D0=B1?= =?utf8?q?=D0=BB=D0=B8=D0=BA=D0=BE=D0=B2=D0=B0=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- README.md | 14 + .../README.md" | 1 + ...\320\275\320\270\320\265\320\274 btrfs.md" | 264 ++++++++++++++++++ 3 files changed, 279 insertions(+) create mode 100644 README.md create mode 120000 "SDL/2022-03 \320\237\320\276\320\264\320\263\320\276\321\202\320\276\320\262\320\272\320\260 \320\276\320\272\321\200\321\203\320\266\320\265\320\275\320\270\321\217 \320\264\320\273\321\217 \320\270\321\201\321\201\320\273\320\265\320\264\321\203\320\265\320\274\320\276\320\263\320\276 \320\277\321\200\320\276\321\206\320\265\321\201\321\201\320\260 \321\201 \320\270\321\201\320\277\320\276\320\273\321\214\320\275\320\276\320\262\320\260\320\275\320\270\320\265\320\274 btrfs/README.md" create mode 100644 "SDL/2022-03 \320\237\320\276\320\264\320\263\320\276\321\202\320\276\320\262\320\272\320\260 \320\276\320\272\321\200\321\203\320\266\320\265\320\275\320\270\321\217 \320\264\320\273\321\217 \320\270\321\201\321\201\320\273\320\265\320\264\321\203\320\265\320\274\320\276\320\263\320\276 \320\277\321\200\320\276\321\206\320\265\321\201\321\201\320\260 \321\201 \320\270\321\201\320\277\320\276\320\273\321\214\320\275\320\276\320\262\320\260\320\275\320\270\320\265\320\274 btrfs/\320\237\320\276\320\264\320\263\320\276\321\202\320\276\320\262\320\272\320\260 \320\276\320\272\321\200\321\203\320\266\320\265\320\275\320\270\321\217 \320\264\320\273\321\217 \320\270\321\201\321\201\320\273\320\265\320\264\321\203\320\265\320\274\320\276\320\263\320\276 \320\277\321\200\320\276\321\206\320\265\321\201\321\201\320\260 \321\201 \320\270\321\201\320\277\320\276\320\273\321\214\320\275\320\276\320\262\320\260\320\275\320\270\320\265\320\274 btrfs.md" diff --git a/README.md b/README.md new file mode 100644 index 0000000..bd71e19 --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ + +Данный репозиторий содержит статьи написанные Николаем Шапловым, aka dhyannataraj (если не указано иное) + +Статьи из данного репозитория распространяются на условиях лицензии CC BY (Разрешено любое переиспользование и создание производных работ при условии указания авторства оригинала), если в тексте статьи или сопроводительной документации группы статей не указано иное. +Однако автор будет признателен, если авторская позиция по вопросам Свободного ПО, Публичных стандартов и прочим фундаментальным вопросам, в производных работах была сохранена и донесена до читателя. + +# Об авторе + +Шаплов Николай Николаевич + +По состоянию на 2025 эксперт в области фаззинга, рботающий в компании Postgres Professional. +Perl-программист с многолетним стажем. +Последовательный сторонник Свободного ПО, Публичных Стандартов и децентрализованных сервисов. + diff --git "a/SDL/2022-03 \320\237\320\276\320\264\320\263\320\276\321\202\320\276\320\262\320\272\320\260 \320\276\320\272\321\200\321\203\320\266\320\265\320\275\320\270\321\217 \320\264\320\273\321\217 \320\270\321\201\321\201\320\273\320\265\320\264\321\203\320\265\320\274\320\276\320\263\320\276 \320\277\321\200\320\276\321\206\320\265\321\201\321\201\320\260 \321\201 \320\270\321\201\320\277\320\276\320\273\321\214\320\275\320\276\320\262\320\260\320\275\320\270\320\265\320\274 btrfs/README.md" "b/SDL/2022-03 \320\237\320\276\320\264\320\263\320\276\321\202\320\276\320\262\320\272\320\260 \320\276\320\272\321\200\321\203\320\266\320\265\320\275\320\270\321\217 \320\264\320\273\321\217 \320\270\321\201\321\201\320\273\320\265\320\264\321\203\320\265\320\274\320\276\320\263\320\276 \320\277\321\200\320\276\321\206\320\265\321\201\321\201\320\260 \321\201 \320\270\321\201\320\277\320\276\320\273\321\214\320\275\320\276\320\262\320\260\320\275\320\270\320\265\320\274 btrfs/README.md" new file mode 120000 index 0000000..6b6c5b4 --- /dev/null +++ "b/SDL/2022-03 \320\237\320\276\320\264\320\263\320\276\321\202\320\276\320\262\320\272\320\260 \320\276\320\272\321\200\321\203\320\266\320\265\320\275\320\270\321\217 \320\264\320\273\321\217 \320\270\321\201\321\201\320\273\320\265\320\264\321\203\320\265\320\274\320\276\320\263\320\276 \320\277\321\200\320\276\321\206\320\265\321\201\321\201\320\260 \321\201 \320\270\321\201\320\277\320\276\320\273\321\214\320\275\320\276\320\262\320\260\320\275\320\270\320\265\320\274 btrfs/README.md" @@ -0,0 +1 @@ +Подготовка окружения для исследуемого процесса с испольнованием btrfs.md \ No newline at end of file diff --git "a/SDL/2022-03 \320\237\320\276\320\264\320\263\320\276\321\202\320\276\320\262\320\272\320\260 \320\276\320\272\321\200\321\203\320\266\320\265\320\275\320\270\321\217 \320\264\320\273\321\217 \320\270\321\201\321\201\320\273\320\265\320\264\321\203\320\265\320\274\320\276\320\263\320\276 \320\277\321\200\320\276\321\206\320\265\321\201\321\201\320\260 \321\201 \320\270\321\201\320\277\320\276\320\273\321\214\320\275\320\276\320\262\320\260\320\275\320\270\320\265\320\274 btrfs/\320\237\320\276\320\264\320\263\320\276\321\202\320\276\320\262\320\272\320\260 \320\276\320\272\321\200\321\203\320\266\320\265\320\275\320\270\321\217 \320\264\320\273\321\217 \320\270\321\201\321\201\320\273\320\265\320\264\321\203\320\265\320\274\320\276\320\263\320\276 \320\277\321\200\320\276\321\206\320\265\321\201\321\201\320\260 \321\201 \320\270\321\201\320\277\320\276\320\273\321\214\320\275\320\276\320\262\320\260\320\275\320\270\320\265\320\274 btrfs.md" "b/SDL/2022-03 \320\237\320\276\320\264\320\263\320\276\321\202\320\276\320\262\320\272\320\260 \320\276\320\272\321\200\321\203\320\266\320\265\320\275\320\270\321\217 \320\264\320\273\321\217 \320\270\321\201\321\201\320\273\320\265\320\264\321\203\320\265\320\274\320\276\320\263\320\276 \320\277\321\200\320\276\321\206\320\265\321\201\321\201\320\260 \321\201 \320\270\321\201\320\277\320\276\320\273\321\214\320\275\320\276\320\262\320\260\320\275\320\270\320\265\320\274 btrfs/\320\237\320\276\320\264\320\263\320\276\321\202\320\276\320\262\320\272\320\260 \320\276\320\272\321\200\321\203\320\266\320\265\320\275\320\270\321\217 \320\264\320\273\321\217 \320\270\321\201\321\201\320\273\320\265\320\264\321\203\320\265\320\274\320\276\320\263\320\276 \320\277\321\200\320\276\321\206\320\265\321\201\321\201\320\260 \321\201 \320\270\321\201\320\277\320\276\320\273\321\214\320\275\320\276\320\262\320\260\320\275\320\270\320\265\320\274 btrfs.md" new file mode 100644 index 0000000..5a4c820 --- /dev/null +++ "b/SDL/2022-03 \320\237\320\276\320\264\320\263\320\276\321\202\320\276\320\262\320\272\320\260 \320\276\320\272\321\200\321\203\320\266\320\265\320\275\320\270\321\217 \320\264\320\273\321\217 \320\270\321\201\321\201\320\273\320\265\320\264\321\203\320\265\320\274\320\276\320\263\320\276 \320\277\321\200\320\276\321\206\320\265\321\201\321\201\320\260 \321\201 \320\270\321\201\320\277\320\276\320\273\321\214\320\275\320\276\320\262\320\260\320\275\320\270\320\265\320\274 btrfs/\320\237\320\276\320\264\320\263\320\276\321\202\320\276\320\262\320\272\320\260 \320\276\320\272\321\200\321\203\320\266\320\265\320\275\320\270\321\217 \320\264\320\273\321\217 \320\270\321\201\321\201\320\273\320\265\320\264\321\203\320\265\320\274\320\276\320\263\320\276 \320\277\321\200\320\276\321\206\320\265\321\201\321\201\320\260 \321\201 \320\270\321\201\320\277\320\276\320\273\321\214\320\275\320\276\320\262\320\260\320\275\320\270\320\265\320\274 btrfs.md" @@ -0,0 +1,264 @@ +# Подготовка окружения для исследуемого процесса с испольнованием btrfs + +**Update:** По состоянию на 2025 год, коллеги из БазАльта предложили более совершенный способ переключения между снепшотами, основанный на обращении к соответствующим системным вызовам ядра прямо из кода исследуемой программы, в самом ее начале. Это позволяет реализовать фаззинг со снепшотами каталога данных на основе свободного AFL++. Описание подхода находится [Здесь](https://www.altlinux.org/PostgreSQL16_fuzzing_technique) ([WebArchive](http://web.archive.org/web/20241203093118/https://www.altlinux.org/PostgreSQL16_fuzzing_technique)) + + +--- + +Часто при фаззинге сложных проектов может оказаться, что для нормальной работы каждому экземпляру исследуемой программы нужна собственная директория с данными, которую не плохо бы перед каждым новым прогоном исследуемой программы возвращать в исходное состояние. В данном посте в формате "заметок" я расскажу как мне удалось сделать восстановление окружения при фаззинге postgres'а, используя btrfs под фаззером Crusher. + +Для фаззига postgres'а система восстановления контекста чрезвычайно важна. Полноценный postgres не может работать без своего личного хранилища, и при этом меняет его практически на каждый чих. Без возвращения хранилища к исходному состоянию при тестировании, ни о какой воспроизводимости и речи быть не может. Поэтому как только заходит речь о фаззинг-тестировании postgres'а в сборе, необходимо сразу думать о системе восстановления контекста. + +## btrfs -- файловая система со снепшотами + +В качестве framework'а для системы восстановления контекста была выбрана файловая система btrfs. Btrfs позволяет эффективно работать со снепшотами на уровне директорий. А именно, позволяет в рамках файловой системы получить клон указанной директории с минимальными затратами. Клонирование происходит на логическом уровне, данные по факту не копируются, и только лишь изменения внесенные в копию или в оригинал занимают дополнительное место на диске. Все это позволяет экономить время и дисковое пространство. + +Фаззинг, при его промышленном применении, имеет практический смысл, будучи запущенным во много потоков на многих процессорах. Для каждого инстанса фаззера, а точнее для исследуемой программы запущенной этим фаззером, предполагается создать свой собственный снепшот с уникальным именем, содержащий данные необходимые для запуска исследуемой программы, и возвращать этот снепшот к исходному состоянию перед каждым новым прогоном исследуемой программы, чтобы следы оставленные в хранилище предыдущими прогонами не оказывали влияния на последующее.. + +Мной был написан скрипт `snapshooter.pl` (код приведен в конце поста) реализующий действия со снепшотами, необходимые для фаззинга. Скрипту первым параметром передается имя команды и далее параметры которые нужны команде для работы. + +Ниже приведены команды и их описания: + +###### `init MOUNT_POINT IMAGE_FILE IMAGE_SIZE IMAGE_COUNT` + +Принимает 4 аргумента: точка монтирования `MOUNT_POINT`, путь к создаваемому образу файловой системы `IMAGE_FILE`, предполагаемый размер одного снепшота `IMAGE_SIZE` и ожидаемое количество снепшотов `IMAGE_COUNT`. + +Скрипт отмонтирует то, что было примантировано к точки монтирования, создаст новый образ файловой системы, примонтирует его к точке монтирования, и создаст в файловой системе директорию `_reference`. + +В эту директорию следует скопировать "эталонный" снепшот, на базе которого будут строиться снепшоты для всех экземпляров + +###### `clone MOUNT_POINT SNAPSHOT_NAME` + +Принимает два аргумента: точку монтирования `IMAGE_COUNT` и имя снэпшота `SNAPSHOT_NAME`. + +Команда `clone` клонирует эталонный снепшот в директорию `[SNAPSHOT_NAME].base`. Если есть потребность внести какие-то специфичные для инстанса изменения в снепшот, то это следует сделать в этой директории. В дальнейшем перед каждым запуском исследуемой программы рабочий снепшот будет приводится к "базововму" состоянию. + +###### `reset MOUNT_POINT SNAPSHOT_NAME` + +Принимает два аргумента: точку монтирования `MOUNT_POINT`и имя снэпшота `SNAPSHOT_NAME`. + +Команда `reset` удаляет снепшот `SNAPSHOT_NAME`, и создает новый с таким же именем клонируя базовый снепшот `[SNAPSHOT_NAME].base`. Таким образом происходит восстановление целевого снепшота исходное состояние. + +В дальнейшем, для ускорения работы, планируется образ файловой системы со снепшотами перенести с диска в память, но я пока еще не разбирался как это делается. + +## Crusher + +В Crusher 2.11.0 появились инструменты позволяющие инициировать подготовку, и восстановления окружения для запускаемых процессов. К сожалению для того чтобы задействовать эти инструменты необходимо написание скриптов на языке python (и ни на каком другом), а я этот python не умею, и честно говоря не очень хочу уметь. К счастью коллеги снабдили меня обертками которые позволяют из непонятного питона запускать понятные `.sh` и прочие скрипты. Поэтому конструкция получилась двухсоставная. + +Предложенное решение использует два "хука" предоставляемые крашером. Первый хук дергается при создании нового инстанса фаззера (см. `--configurator-script` в документации), и используется для создания нового снепшота с данными хранилища, которыми будет пользоваться исследуемая программа. Второй хук дергается перед каждым новым прогоном исследуемой программы (см. `--environment-plugin` в документации), и используется для того чтобы вернуть снепшот с хранилищем в исходное состояние (ибо есть шанс что при предыдущих запусках он мог быть иземенен) + +### Подготовка окружения + +Изначальная подготовка окружения осуществляется через скрипт `conf.py`: + +```python +import json +import os +import sys +import traceback +import subprocess + +def transform_options(ops_json): + try: + jops = json.loads(ops_json) + instance_name = jops['configuration']['instance_name'] + this_dir = os.path.dirname(__file__) + script_path = this_dir + '/init_script.sh' + pr = subprocess.Popen([script_path, instance_name], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + pr.wait() + assert pr.returncode == 0 + output = pr.stdout.read() + for line in output.split(): + ls = line.split('=') + assert len(ls) == 2 + var_name = ls[0].strip() + var_value = ls[1].strip() + os.environ[var_name] = var_value + return json.dumps(jops) + except Exception as ex: + print("EXCEPTION!") + traceback.print_exc() + return None +``` +Который в свою очередь вызывает скрипт `init_script.sh`: + +```bash +#!/bin/bash + +INSTANCE_NAME=$1 +THIS_DIR=$(dirname $0) + +sudo ./snapshooter.pl clone mnt "${INSTANCE_NAME}" 1>&2 || exit 1 +sudo ./snapshooter.pl reset mnt "${INSTANCE_NAME}" 1>&2 || exit 1 + +echo "PGDATA=${THIS_DIR}/mnt/$INSTANCE_NAME" +``` + +Скрипту `init_script.sh` в качестве аргумента передается уникальное имя инстанса фаззера, это имя скрипт использует в качестве имени создаваемого снепшота. Кроме того на `STDOUT` скрипт выводит пары `NAME=VALUE` переменных окружения которые должны быть установлены при запуске. В нашем случае мы через переменную окружения сообщаем постгресу где находится его хранилище. Вывод скрипта анализируется оберткой `env.py` и значения переменных окружения передаются фаззеру для последующего применения. + +При запуске фаззера необходимо указать расположение скрипта `conf.py` через параметр `--configurator-script ` + +### Восстановление окружения +Идея та же, что и с созданием окружения. Питоновская обертка `env.py`: + +```python +import json +import os + +error_msg = '...' + +def get_error(): + return error_msg + +instance_name = None + +def init(json_options): + jops = json.loads(json_options) + global instance_name + instance_name = jops['configuration']['instance_name'] + return True + +def finish(): + return True + +def setup(): + this_dir = os.path.dirname(__file__) + script_path = this_dir + '/run_script.sh' + cmd = script_path + ' ' + instance_name + r = os.system(cmd) + return r == 0 + +def teardown(): + return True +``` +И `.sh` скрипт выполняющий содержательную работу: +```bash +#!/bin/bash + +INSTANCE_NAME=$1 + +sudo ./snapshooter.pl reset mnt "${INSTANCE_NAME}" 1>&2 || exit 1 +``` +В данном случае мы получаем имя инстанса в качестве аргумента и восстанавливаем снепшот с этим именем в исходное состояние. + +При запуске фаззинга расположение файла `env.py` указывается через параметр конфигурации `--environment-plugin` + +### Запуск фаззинга +Перед запуском фаззинга мы должны инициализировать хранилище снепшотов, раздать эталонному снепшоту правильные права (постгрес требует чтобы хранилище не было доступно на чтение кому либо кроме постгреса) и заполнить правильными данными. Заранее подготовленный снимок хранилища у меня лежит в директории `etalon`, я его просто копирую. Ну а дальше все достаточно просто. + +```bash +#!/bin/bash + +ME=`whoami` +sudo ./snapshooter.pl init mnt image.img 40000000 10 +sudo chown $ME: mnt/_reference +chmod 700 mnt/_reference + +cp -r etalon/* mnt/_reference + +~/crushers/latest/bin_x86-64/fuzz_manager --bitmap-size 300000 --start 4 --eat-cores 1 --dse-cores 0 -i in -o out -T StdIn -F -I StaticNoForkSrv --configurator-script conf.py --environment-plugin env.py -- /home/nataraj/tests/fuzz_psql/pg/bin/postgres --single postgres +``` + +Таким образом удалось успешно запустить простейший параллельный фаззинг SQL-запросов с восстановлением состояния хранилища к исходному перед каждым запуском. На сам по себе прямой фаззинг SQL-запросов я больших надежд не возлагаю, фаззить в лоб сложные синтаксические конструкции -- малопродуктивно, однако система восстановления контекста в дальнейшем будет применена для фаззинг-исследования функциональности потенциально приводящей к модификации хранилища postgresql. + +## Приложения +### snapshooter.pl +```perl +#!/usr/bin/perl + +use strict; + +my $command = shift @ARGV; + +die "Укажите команду первым аргументом" unless $command; + +if ($command eq "init") +{ + my $mount_path = shift @ARGV; + my $image_path = shift @ARGV; + my $snapshot_size = shift @ARGV; + my $snapshots_count = shift @ARGV; + + unless ($image_path && $mount_path && $snapshot_size && $snapshots_count) + { + die "Команде init нужны 4 параметра: точка монтирования,путь к образу, размер снэпшота и из кол-во"; + } + + `mountpoint -q $mount_path`; + unless( $?) + { + print "$mount_path -- примонтирован. Пробуем отмонтировать...\n"; + `umount $mount_path`; + } + + my $min_btrfs_size = 114294784; + if ($snapshot_size * $snapshots_count < $min_btrfs_size) + { + print "Увеличиваем размер образа до минимально возможных $min_btrfs_size байт"; + $snapshot_size = $min_btrfs_size; + $snapshots_count = 1; + } + + print "Создаем и форматируем образ...\n"; + `dd if=/dev/zero of=$image_path count=$snapshots_count bs=$snapshot_size`; + `mkfs.btrfs $image_path`; + + print "Монтируем образ...\n"; + `mkdir -p $mount_path`; + `mount $image_path $mount_path`; + + print "Создаем reference снэпшот...\n"; + `btrfs subvolume create $mount_path/_reference`; +} + +if ($command eq "clone") +{ + my $mount_path = shift @ARGV; + my $target = shift @ARGV; + + unless ($target) + { + die "Команде $command нужны 2 параметра: точка монтирования, имя снэпшота"; + } + + my $name = "$mount_path/$target.base"; + + if (-e $name) + { + die "Снэпшот $name уже существует"; + } + print "Создаю основу для снэпшота $target: $name\n"; + + `btrfs subvolume snapshot $mount_path/_reference $name`; +} + +if ($command eq "reset") +{ + + my $mount_path = shift @ARGV; + my $target = shift @ARGV; + + unless ($target) + { + die "Команде $command нужны 2 параметра: точка монтирования, имя снэпшота"; + } + my $name = "$mount_path/$target"; + unless (-e "$name.base") + { + die "Не существует основы для снэпшота $target: $name.base"; + } + + print "Обновляю снэпшот $target: \n"; + + if (-e $name) + { + `rm -r $name`; + print "Удаляем старый\n"; + } + + print "Создаем новый\n"; + + `btrfs subvolume snapshot $name.base $name`; +} +``` + -- 2.47.3