Pull to refresh

Создание JPEG из ниоткуда

Reading time 4 min
Views 22K
Original author: Michal Zalewski
Вот интересная демонстрация возможностей afl; меня реально удивило, что она работает!

$ mkdir in_dir
$ echo 'hello' >in_dir/hello
$ ./afl-fuzz -i in_dir -o out_dir ./jpeg-9a/djpeg

В сущности, я создал текстовый файл только со словом "hello" и попросил фаззер выдавать поток в программу, которая ожидает на входе изображение JPEG (djpeg это простая утилита, которая идёт вместе с распространённой графической библиотекой IJG jpeg; libjpeg-turbo тоже должна подойти). Конечно, мои входные данные не похожи на валидное изображение, так что утилита быстро отвергает их:

$ ./djpeg '../out_dir/queue/id:000000,orig:hello'
Not a JPEG file: starts with 0x68 0x65

Обычно такой фаззинг был бы совершенно бессмысленным: по существу, нет никаких шансов, что традиционный форматонезависимый фаззер способен когда-нибудь превратить слово "hello" в действительное изображение JPEG. Вероятность того, что десятки случайных настроек выстроятся друг за другом, астрономически мала.

К счастью, afl-fuzz может использовать в своих целях простой инструментарий на уровне ассемблера — и в течение миллисекунды или около того он замечает, что хотя установка первого байта в значение 0xff не изменяет внешне наблюдаемый вывод, можно запустить немного иной внутренний путь в тестовом приложении. Обладая этой информацией, он решает использовать этот тестовый случай как основу для будущих раундов фаззинга:

$ ./djpeg '../out_dir/queue/id:000001,src:000000,op:int8,pos:0,val:-1,+cov'
Not a JPEG file: starts with 0xff 0x65

Обрабатывая затем тестовый случай второго поколения, фаззер почти сразу замечает, что установка второго байта в значение 0xd8 делает нечто даже более интересное:

$ ./djpeg '../out_dir/queue/id:000004,src:000001,op:havoc,rep:16,+cov'
Premature end of JPEG file
JPEG datastream contains no image

Здесь фаззер умудрился синтезировать валидный заголовок файла — и действительно понял его значимость. Используя такую выдачу как основу для следующего раунда фаззинга, он быстро начинает погружаться всё глубже и глубже в суть. Через несколько сотен поколений и несколько сотен миллионов вызовов execve() он находит всё больше и больше управляющих структур, которые необходимы для валидного файла JPEG — SOF'ы, таблицы Хаффмана, таблицы квантования, маркеры SOS и т. д.:

$ ./djpeg '../out_dir/queue/id:000008,src:000004,op:havoc,rep:2,+cov'
Invalid JPEG file structure: two SOI markers
...
$ ./djpeg '../out_dir/queue/id:001005,src:000262+000979,op:splice,rep:2'
Quantization table 0x0e was not defined
...
$ ./djpeg '../out_dir/queue/id:001282,src:001005+001270,op:splice,rep:2,+cov' >.tmp; ls -l .tmp
-rw-r--r-- 1 lcamtuf lcamtuf 7069 Nov  7 09:29 .tmp

Первая картинка, полученная через шесть часов фаззинга на 8-ядерной системе, выглядит весьма скромно: это чистый серый прямоугольник высотой 3 пикселя и шириной 748 пикселей. Но с момента её открытия фаззер начинает использовать эту картинку как основу — и быстро производит широкий спектр более интересных картинок для каждого нового пути исполнения:



Конечно, синтез полного изображения из ниоткуда — это исключительный случай, и вряд ли полезный на практике. Но в более прозаичных целях фаззеры подходят для нагрузочного тестирования любой функции в целевой программе. Оборудованный оснасткой, эволюционный фаззинг с использованием менее известных функций (например, JPEG с прогрессивным или арифметическим кодированием, чёрно-белые JPEG) можно использовать как альтернативу гигантскому высококачественному корпусу разнообразных тестовых случаев, с которых начинается фаззинг.

Замечательной особенностью случая с libjpeg является то, что он работает без какой-либо специальной подготовки: в строке "hello" нет ничего особенного, фаззер ничего не знает о парсинге изображений, он не предназначен и не настроен для работы конкретно с этой библиотекой. Нет даже каких-то ключей командной строки, которые следует активировать. Вы можете натравить afl-fuzz на многие другие виды парсеров с такими же результатами: с bash он будет писать валидные скрипты; с giflib производить GIF'ы; с fileutils производить файлы ELF и выставлять флаги, создавать бинарники для Atari 68xxx, загрузочные секторы x86 и UTF-8 с BOM. Почти во всех случаях влияние оснастки на производительность тоже минимально.

Конечно, не всё так гладко. По сути своей afl-fuzz остаётся программой для брутфорса. Это делает её простой, быстрой и надёжной, но также означает, что определённые типы атомизированных проверок в большом пространстве поиска могут стать непреодолимым препятствием для фаззера. Вот хороший пример:

if (strcmp(header.magic_password, "h4ck3d by p1gZ")) goto terminate_now;

На практике это означает, что afl-fuzz вряд ли сумеет «изобрести» с нуля файлы PNG или нетривиальные документы HTML — и ему нужна начальная точка получше, чем просто "hello". Чтобы неизменно работать с конструкциями кода как в вышеприведённом примере, универсальному фаззеру нужно понимать работу целевого бинарника совершенно на ином уровне. Учёные добились некоторого прогресса в этом отношении, но нам придётся ещё годы ждать появления фреймворков, которые способны быстро, просто и надёжно работать с разнообразными и сложными кодовыми базами.

Несколько человек спросили меня о символическом исполнении и других вещах, под влиянием которых создан afl-fuzz; я собрал некоторые заметки в этом документе.
Tags:
Hubs:
+54
Comments 21
Comments Comments 21

Articles