• XSS.stack #1 – первый литературный журнал от юзеров форума

прочее Софт от swagcat228 (QAFEL: Qemu AFL Force Extendded Layer)

swagcat228

X-pert
Эксперт
Регистрация
23.12.2019
Сообщения
284
Реакции
232
Депозит
300
http://**************************************************************/git/swagcat228/qafel.git

Собсно говоря.

В виду критической нагрузки и систематической нехватки времени - если кому-то будет интересно и/или будут возникать вопросы, (а если первое условие истинно, то вопросы возникать будут) - милости прошу в ишьюсы, по возможности всем отвечу.
PR так же приветствуются.
 
Пожалуйста, обратите внимание, что пользователь заблокирован
http://**************************************************************/git/swagcat228/qafel.git

Собсно говоря.

В виду критической нагрузки и систематической нехватки времени - если кому-то будет интересно и/или будут возникать вопросы, (а если первое условие истинно, то вопросы возникать будут) - милости прошу в ишьюсы, по возможности всем отвечу.
PR так же приветствуются.
А можешь оформить пост тут /threads/29390/ ? чтобы не затерялось среди других сообщений.
 
Пожалуйста, обратите внимание, что пользователь заблокирован
А можешь оформить пост тут /threads/29390/ ? чтобы не затерялось среди других сообщений.
Или если хочешь можешь отдельным топиком оформить /forums/145/
 
Да я думаю буду писать по чуть-чуть по мере возможности. Где именно не принципиально.

И так фаззинг.
1711109049694.png


25 лет назад найти информацию на какую-либо тему в сфере компьютерного искусства было не так-то просто. Был такого себе рода информационный вакуум.
Молодые и идейный ребята из штатов на тот момент писали книги, где расписывали интерналсы чипсетов и устройства шины PCI для программистов, где между строк виднелись отголоски к способам взлома отрицательных колец винды.

Сегодня нейросети пофиксили даже эти книги в PDF. От книг осталась половина. В прямом смысле.
А все информационное пространство переполнено шумом, и наша задача в этом шуме найти крупицу нужного смысла.

  • Виды фаззинга.
Различать подходы к сабжу можно по ряду признаков. Обычно его делят на две основные категории - `ковераж бэйзд` или `ковераж гайдед` фаззинг -- то бишь фаззинг со сбором покрытия, и все остальное.
Нам интересен только первый. Покрытие - это трассировка `контрол флоу` -- след выполнения программы, запечатленный в виде захешированных `эджэс` (углы, или ребра) и записанных на карту покрытия.
Представь себе тетрадный лист в клеточку, или шахматную доску. Только очень большую, минимум на 64кб клеточек. Так вот каждый раз, когда код переходит к новому блоку ветвления - образовывается новый угол.
То есть с точки зрения потока выполнения любой новый бранч в самой первой своей инструкции, вернее её адресе, образует некий хеш с адресом первой инструкции предыдущего блока. После чего этот хеш записывается в клеточку такого номера, какой несёт в себе сам этот хеш.
Ну к примеру, если мы были в 0x11229920, но первая инструкция (начало бранча) была в 0x11223344, и из адреса 0x11229920 мы прыгнули по адресу 0x55667788 (начало второго бранча) то наш хеш покрытия будет к примеру `0x11223344 ^ 0x55667788` - самый примитивный способ получения хеша угла.
Проблема тут в том, что такое-же значение может (и обязательно будет) получено при вычислении других двух адресов -- произойдет так называемая коллизия. В общем случае избавить от коллизий не возможно, т.к. что бы это сделать нужно такую большую карту покрытия, что мы будем все процессорное время тратить на ее сканирование после каждой итерации.
Так что просто смирись, что некоторая потеря точности тут будет и это нормально. В общем этот процесс называется инструментацией кода - то есть в код исследуемой программы некоторым образом встраивается логика, которая записывает эти хеши в процессе выполнения самой программы.
Инструментация тоже делится на несколько видов, я бы выделил три основных: Статичный, Динамичный, и Аппаратный. На этапе компиляции, на этапе выполнения, и используя аппаратные возможности процессора Intel (да-да, кроме аппаратных закладок там еще есть и полезные фичерзы =D), соответственно.
Лично я предпочитаю второй вид, динамический, если доступа к исходникам нету разумеется. А в общем случае их и не будет, а если и будут то то, что ты из них скомпилишь, может сильно отличаться от того, что из них скомпилили в проде собранном из говна и палок, но об этом потом.
Есть масса сравнений и разборов в инете, таблички соотношения скорости и точности и прочее, заострять тут внимание не будем, разве что добавлю что аппаратную инструментацию можно юзать только при фаззинге нативной (i386/amd64) архитектуры.

Динамическая инструментация "разбавляет" собой ББ (базовые блоки, они же бранчи) добавляя перед первой инструкцией блока некий кодес, который будет вычислять свой хеш и записывать его в тетрадный лист, о котором говорили выше.
Мой любимый бекенд - QEMU-AFL.
В движке КЕМУ-АФЛя это представлено примерно вот -> так <-.
Кстати, забыл представить - AFL++ -- заслуженный временем, кстати американский, но тщательно доработанный немецким напильником, фаззи луп =)
У афля (фронтенд) есть множество бекендов, начиная от непосредственной инструментации на этапе сборки (вайт-бокс фаззинг) когда инструментирующая логика вкомпилирывается в код прям при сборке, заканчивая AFL-NYX - это снапшот бейзед, хардваре ковераж гайдед фаззинг. Самый модный, но абсолютно не рабочий из коробки.
Из рабочих фаззеров с аппаратным сбором покрытия (линушных) есть только старый kAFL(k - kernel). Самый первый написанный на python2. Вот тот еще работал, дальше все попса и суррогат. Ну и там всякие WTF (Вот Зэ Фазз), но то винда.

Ах, да, есть еще один важный момент, который нужно освятить в данной части этого... материала. Тип цикла. Особо важное значение это играет для блек-бокс фаззинга.
Цикл может быть персистент, или не персистент. Персистент цикл может быть со снапшотами и без них.
К примеру есть у нас в таргете функция, которая отвечает за парсинг инпута. Мы можем "обвязать" эту функцию в персистент луп, таким образом, что таргет на выходе из этой функции будет возвращаться к ее началу. В начале этой функции так же будет дополнительная инструментация, которая будет подавать на функцию ввод в начале каждого цикла.
Плюсы очевидны - скорость, стабильность покрытия (детерминированный контрол флоу), отсутствие затрат машинного времени на прохождение остальных веток кода.
Минусы - очень синтетический сценарий, чаще будет давать не такой хороший результат как живой фаззинг для блек-бокс кампаний. Почему? И почему именно для блек-бокс? Потому, что при вайт-бокс мы инструментируем код не только сборщиками покрытия, но и санитайзерами. И любое малейшее прикосновение к памяти за пределами буфера, или потеря указателя (для плюсовых), или UAF -- это тут же будет срабатывание санитайзера. В блэк-бокс кампаниях разбавить код санитайзером так просто не получится. Есть попытки это сделать, QASAN, но они остановились на этапе попыток, поскольку в общем случае это не возможно, т.к. мы не имеем представления о работе аллокатора в целевом приложении, особенно если оно собрано статично, или если оно не нативной архитектуры. Уж тем более мы не знаем что там под какую переменную на стеке выделено было, и где границы буферов. Так что, если наш инпут зацепит какой-то баг, скажем UAF, в текущем цикле -- то это вообще не гарантирует того, что наш таргет в этом же цикле и крашнется. И что он вообще крашнется.


1711121418252.png
  • Наводим резкость
Как сказал один мудрец - супе-спец знает все ни о чем. Типа сужается вектор =)
Так вот, остановились мы на том, что в реальной жизни, особенно при лайв фаззинге, у тебя будет в папке крашей куча таких крашей которые не воспроизводятся. То есть ты скармливаешь их приложению, а оно не падает.
Что же делать? Логировать! В движке (фронте) алфя есть такая великолепная фича, как -> PERSISTENT_RECORD <-.
По умолчанию она почему-то выключенна, и там надо было что-то дополнительно делать что бы ее включить, уже не припомню, не суть, суть в том, что эта фича держит в конвейере последние N инпутов, и в момент когда происходит крэш конвейер -> сбрасывается <- в папочку с крашами.
Гениально и просто. На практике я обычно ставлю 10 последних инпутов. Если даже какой-то из таких краш-чейнов не сработает - в любом случае будут повторы. То есть одни и те же баги будут проявляться с разными инпутами, поскольку будут иметь разную карту покрытия.
Иногда это становится проблемой, особенно когда работаешь с большим, жирным, приложением, особенно под x64, у которого, с*ка, 40 потоков, и он хендлит входящие соединения методом поллинга асинхронных (не блокирующих) сокетов. Детерменировать такое дюже не просто.

Что значит детерменировать такое, и какое вообще надо детерменировать, и почему это надо делать, и наконец, как это надо делать? А как это делать не надо?
Помнишь выше за листочек в клеточку разговаривали?
Вот -> тут <- голова зануляет карту покрытия в начале цикла. Дальше пишет в пайп, что бы ответная часть (форк-сервер)* прочел из пайпа и начинал новую итерацию.
Вот -> тут <- форк-чайлд пишет (ну когда-то писал, в стоке лучше глянуть этот момент там этот макрос определен понятнее) покрытие в карту покрытия *.
Вот -> тут <- голова проводит подсчет очков набранных за текущую итерацию.

Кстати, там интересная логика подсчёта. Рекомендую ознакомиться, особенно математикам. Вообще математика мне не хватает в системном программировании.

Форксервер - историческое название, когда-то люди (девочка, не помню имени) придумали делать fork() в начале цикла, и фаззить потомка. Что бы не перезапускать все приложение каждый раз с нуля
Карта покрытия - это страница межпроцессовой памяти, SHM сегмент так называемый, доступный одновременно и фронтенду и бекенду.. Изменения в такой памяти синхронизированны (условно) между процессами.

Теперь внимание.
Есть однонитьевый фронт и 40-поточный бэк.
Бэк получает инпут и выполняет его обработку одним или несколькими из 40 потоков.
В это время другие потоки дорабатывающие не интересные, в контексте фаззинга, остаточные, работы от предыдущей итерации -- так же генерируют покрытие.
Покрытие (в стоке) генерируется любым потоком в любой момент времени.
Это аксиома.
И это проблема.
Конечно, стоковый дизайн афля не предполагает фаззинг 40 нитьевых таргетов, еще и в лайв режиме.
Там дизайн предполагает, что точка входа в форксервер будет установлена перед входом в функцию парсера.
Далее форксервер форкается, и засыпает в ожидании сигнала от потомка. Линуксоиды знают, что после форка остаётся единственная нить.
То есть, чайлд всегда будет однонитьевым. Далее, в случае персистента, на выходе из парсера предполагается джамп в начало,
где с помощью либы (специально обученной подгружается в виде модуля), либо патча бинарника в Айде, достигается чтение не из сокета, а из stdin, или вообще shm_map *.
Кроме обязательной shm страницы есть еще не обязательная - для передачи ввода, вместо stdin.
Имеет смысл если работа происходит с синтетически детерменированым кусочком кода (чисто парсер), что бы избежать затрат на работу с дескрипторами.
В реальности такие парсера встречаются не часто.
Многие из них читают из сокета по чуть-чуть, скажем, по 4 байта, и от этого зависит дальнейшее поведение.
Некоторые делают это одим потоком, переключаясь между пирами ;)

Форксервер -> читает рэп из пайпа <- и продолжает выполнение потока потомка.
В оригинальном AFL при фаззинге на QEMU в кач-ве бекенда топология группы процессов примерно такая:
1711122680554.png
1) То есть бошка стартует сразу два инстанса твоего 40-нитьевого приложения,
2) оба инстанса по дефолту выбирают точкой входа* просто адрес старта бинарника,
3) а дальше инициализируются и спаунят, вернее спаунит обычный форксервер, а редквин пока что ждет без чайлда, чайлда,
4) который уже запускается, прогружается, в себе создаёт свои 40 потоков и наконец-то принимает инпут в сетевой сокет,
5) но делает это каким-то одним из своих 40 потоков.

Точка входа в данном контексте это адрес, на котором форксервер инстументирует хук в функцию afl_forkserver().
Выбор точки входа в стоке прописан -> тут <-
Сама инструментация происходит -> тут <-
А хелпер* прописан -> тут <-

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

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

Так вот, критикуя стоковую реализацию qemuafl можно стереть язык (на критику моей реализации и жизни не хватит =D), по этому рассмотрим основные моменты.
Сперва причины:

1) signal(SIGSTOP / SIGCONT);
2) Отдельные родители (форксервера) для обычной и cmplog версии. (что это такое разберём позже).
3) Отсутствие какого-либо контроля сборки покрытия в многонитьевых приложения.
4) Разного рода баги при работе с памятью, ключевой баг это небезопасное обращение к памяти таргета в хелпере cmplog рантайма.
5) Жирный-жирный баг, мемори лик, который остался до сих пор даже в стоке самой кему, не только кемуафля.
Далее следствия:
1) вся группа нитей получает сигнал SIGSTOP, это меняет поведение (бехейвиор) на многонитьевых процессах.
2) Не возможно запустить две копии одного и того же приложения, биндящего одни и те же порты, на одних и тех же интерфейсах.
3) в тетрадочке будет каша из потоков, фронт не сможет понять что есть реакция на наш инпут, а что шум, после пометит шум как virgin_bits, и мы никогда уже не пройдем по тем маршрутам.
4) Краш на стороне домена (кему). Очень неприятная, скрытая и плохо отлаживается. Особенно в cmplog.
5) жирный мемлик, когда тред гостя помирает - его структура task_struct (в кему) не освобождается и много много КБ памяти остаётся не у дел.​

И основная причина, по которой я стал приверженцем лайв фаззинга - некоторые приложения невозможно (слишком трудно) локализовать до одного потока.​
На сегодня мне пора, продолжим позже =)​
 
Последнее редактирование:
  • Проектируем дизайн

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

Модель для стокового дизайна.

1) фронт пишет в пайп.
2) родитель читает из пайпа.
3) родитель посылает чайлду SIGCONT, если ребенок жив, либо форкается, если ребенок мертв.
4) ребенок выполняет цикл, заходит в хелпер персистент лупа, и сам себе (raise()) отправляет SIGSTOP.
5) родитель получает уведомление о смене состояния потомка (waitpid()) и отправляет статус обратно во фронт, через пайп.
6) фронт обрабатывает результаты выполнения, генерирует новый ввод, и возвращается к пункту 1.

Ещё раз проговорим, что родитель - это однонитьевый процесс остановленный в точке входа в бинарник (либо в точке выставленной в ручную, например после инициализации библиотек, но обязательно ДО порождения в себе новых нитей).
То бишь он ещё не инициализирован как инстанс программы, (позволяет чайлдам проходить проверки на наличее копий, не мешает биндить порты). Таких родителей может быть неограниченное количество.
Его потомок в свою очередь - это уже запущенное приложение слушающие сетевые порты.

Так же учтем что если ты хочешь использовать преимущества cmplog, а ты хочешь, то у тебя будет конкуренция за одни и те же сетевые порты как минимум, между потомками двух под-групп процессов. Но это мы в деталях рассмотрим позже!

Так вот, что же будет гонять в персистент лупах?
Изначально я писал библиотеку, которую подгружал в сам таргет с помощью LD_PRELOAD. Это классика.
Библиотека эта собой представляла ничто иное как сетевой эмулятор - читала из stdin, в начале лупа, внутри лупа подключалась по сокету к таргету, писала в него инпут, некоторое время ждала ответ, при наличии читала его из сокета, (при необходимости пробовала найти в нем инфо-лики, сканируя полученные байты и проверяя являются ли они валидным адресом из собственного адресного пространства), закрывала сокет, и доходила до крайней точки лупа. Конструктор библиотеки (специальная штука вызывающаяся линковщиком при загрузке библиотеки) ставил хуки на нужные функции, такие как bind, что бы проверять аргументы и находить нужный момент (к примеру насильно, кастами, менять порт при вызове bind, что бы избежать конкуренции), спаунить новый тред, который в свою очередь уже шел бы в персистент луп. То есть, в исследуемом приложении всегда было бы на 1 поток больше.

По сути этот дополнительный поток и выполнял роль процесса, бегающего в цикле. Если что, с точки зрения ядра понятие поток и процесс размыто, есть понятие task. Этот таск - может быть единственным в группе, тогда программа однонитьевая, либо их может быть множество. Либо может осуществляться fork(), который в итоге все равно реализован на системном вызове clone(), хоть и есть отдельный системный вызов форк. Но не везде. Я настоятельно рекомендую тебе ознакомиться чем SYS_clone отличается от SYS_fork, если ты не знаешь этого.

Так вот, эта либа работала просто отлично, и я был этому очень рад, нафаззил кучу крашей, чуть не ушел в депрессию пытаясь их разобрать))) и все это продолжалось ровно то того момента, как я повстречал статично слинкованный таргет.
Тут моя белая полоса внезапно оборвалась.
Что же делать? Либу не подгрузишь к статично слинкованному бинарю. Можно пойти по пути, который выбрали ребята из Гугла, для своего hongfuzz, запускать таргет с помощью лоадера, но зачем если можно просто перенести эмуляцию в саму кему, подумал я?

Ах, как я был наивен :)


Моя идея заключалась в том, что бы реализовать эмуляцию на уровне системных вызовов внутри QEMU.
К примеру вот -> тут <- можно пронаблюдать попытку эмуляции чтения из сетевого сокета посредством семейства вызовов recv* .
В QEMU они стекаются в конечном итоге данную функцию.
Мне хотелось исключить необходимость обращаться к ядру, к его сетевому апи, каждый цикл. Это влечет за собой великое множество побочных эффектов.
К примеру, сокеты после виснут в TIME_WAIT и криво написанные таргеты просто не могу уже забиндить порт и виснут в ожидании, а афль молотит циклы, видит что карта покрытия стала нестабильной, помечает блоки в vary map, и в принципе сессию фаззинга можно начинать заново.
Частенько попадались таргеты, которые просто корявые до такой степени, что там течет дескриптор. Представляешь фаззинг сервера, которые не закрывает дескрипторы?
Потом начали попадаться таргеты, которые используют epoll_fd для опроса событий. Это и есть та асинхронность. Я процитирую:

1711200901486.png


Теперь представь, сколько работы необходимо проделать, что бы написать логику вокруг каждого системного вызова семейства epoll_*, что бы корректно обработать любые кейсы реализаций асинхронных сетевых серверов, да так, что бы еще и учесть и предотвратить возможные и ошибки в них, в QEMU, и в ядре!
Честно говоря дойдя до epfd моя энергия начала иссякать. К тому же я понял, что подобные "ускорители" сетевых процессов будут слишком сильно влиять на поведение программы, я молчу про то, что добиться от такой архитектуры стабильности и универсальности будет не просто, если вообще возможно.

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

План проектирования дизайна:
1) Сформируем список задач.
2) Аргументируем и опишем задачи.
3) Подумаем как это реализовать.
4) Сформируем список проблем.
5) Аргументируем список проблем.
6) Подумаем как их решить/не допустить.
7) убедимся, что план выглядит реалистично.

Задачи:
1) Удобный трассировщик заточенный именно под задачи фаззинга, особенно сетевых приложений.
2) Техника Pre и Post точек фаззинга, позволяющая локализовать нужный участок кода в нужный момент времени.
3) Более гибкая система инструментации кода.
4) Сетевая эмуляция.
5) Расширенное логирование падений.
6) Поддержка техник прохождения роад-блоков.


Описание задач:
1) Что бы понимать с чем мы имеем дело нужно увидеть как приложение себя ведёт после запуска. Когда происходит создание сокетов, потоков, возможно запуск сторонних программ, либо вообще сетевая активность вроде подключения к удаленным серверам. Обязательно нужно локализовать интересный нам участок кода, и понять какие потоки участвуют в обработке информации, в какой последовательности, на сколько стабильно это происходит. Все это я бы хотел увидеть в столбик шириной в размер терминала, где была бы информация о выполненных базовых блоках, инструментированных базовых блоках, количестве их выполнения за сеанс, а так же возможность фильтрации выходных данных, т.к. я человек, а не машина, и не смогу эффективно обработать 200 мегабайт трассировочной информации.

2) Что бы избежать лишних вычислений, а так же повысить качество покрытия, необходимо устранить шум. Для этого, используя ведомости полученные в результате анализа трассировочных логов, я хочу задать два исполняемых (гостевого пространства) адреса, определяющих начало и конец "рабочего окна" в цикле. Выполнение в Pre-fuzz точке, провоцирует включения сбора покрытия, в Post-fuzz точке - сбор покрытия завершает. Эти две точки должны вызываться единственный раз за одну итерацию фаззинг-цикла. В начале и в конце.

3) Использовать полученную информацию в совокупности с информацией о целях кампании, что бы исключить не интересующие участки кода из инструментирующего (возможно и трассирующего) алгоритма. То есть если мне интересен парсер json из POST запроса, который обрабатывается libjson.so, то я не хочу что-либо знать о выполнении, и тем более инструментировать libm.so.

4) Логично, что сетевую эмуляцию написать придется. Вопрос в том, где именно она будет работать. Внутри гостевого пространства - по опыту нет. Внутри доменного пространства - по опыту нет. Внутри самого AFL - по опыту тоже нет. Остаётся единственное место - внутри форк-сервера. Так же, мне нужна возможность работать с TCP и UDP протоколами.

5) Для нормальной работы с результатом фаззинга нужно кратко и ёмко понять где он произошел, ещё лучше из-за чего. То есть в папочке crashes я хочу видеть вместо номеров итераций адреса в которых программа была остановлена. Ещё лучше что-то типа списка регистров и их значений, некая титульная карточка к цепочке последних N вводов приведших к падению. Так же, в идеале, способ отбросить путь если на нем уже слишком много однотипных падений.

6) естественно, нету желания в ручную составлять каждый возможный вариант ввода для корпуса. Я хочу, что бы строки и константы извлекались динамично, в рантайме. Для этого существует cmplog - реализация технологии Red Queen. А так же compcov - реализация технологии Intel LAF.


Детали Реализации:
1) Трассируемых элементов будет:
Информация о транслируемых ББ.
Информация о выполняемых ББ.
Информация о потоке выполняющем действие.
Ключевые системные вызовы и их результаты.

Вся эта информация, в цветном варианте для лучшего понимания, распечатывается в канал с учётом многопоточной синхронизации.
При старте определяется тип работы - трассировочный запуск, либо рабочий запуск.
Для рабочего варианта хуки не срабатывают. Сделать это рефлексивно, что бы исключить лишние проверки на каждом шагу.

Для статистически данных о ББ придется добавить счётчики в структуры описывающие ББ - благо работать с ними можно атомарно.

2) добавить новые генераторы хелперов, учесть работу в многопоточном контексте (атомарность для каждого шага), добавить хуки в код транслятора (tcg) который эти хелперы будет вызывать. Добавить чекер состояния и переменные состояния для глобального и трэдового использования.

3) добавить возможность исключить из инструментации адресное пространство по его имени (имя либы, сегмента). Сделать это автоматически, к примеру по паттерну.
Эта задача тянет за собой потребность хранить информацию о загружаемых библиотеках и атрибутах сегментов памяти.

4) сетевой эмулятор должен жить внутри форк-сервера. Должен получать ввод каждой новой итерации, из shm сегмента (stdin исключим сразу, т.к. все равно придется подгружать в память перед отправкой в сокет, хотя можно и vmsplice сделать, но не вижу смысла), выполнять сетевую коммуникацию с потомком, при необходимости обработать ответ. В идеале, обладать каким-то механизмом информирующим о сетевом состоянии потомка: потомок ждёт больше данных опрашивая свой сокет - значит смысла ждать тайм-аут на стороне эмулятора нету, можно закрывать сокет преждевременно, но не саму итерацию. Сделать заглушку для возможности дальнейшего расширения функциональности, обработки ответа от потомка, поиск там утечек информации, либо просто логирование уникальных ответов от сервера - и спровоцировавших вводов.

5) расширить функционал обработчика аварийных сигналов. Сделать это придется на доменной стороне потомка. В QEMU существует специальный код, который обрабатывает исключительные состояния и падения процесса. Туда нужно добавить, во первых, способ коммуникации с предком - предку нужно будет передать информацию о происходящем падении. Для этого использовать лучше всего каналы (pipe), это позволит решить нам множество других проблем в том числе.

6) Сказать твердое НЕТ ненужным группам процессов, и интегрировать функционал CMPLOG, COMPCOV, в один флакон с обычным фаззингом. Все происходит в рамках единого процесса. По опыту знаю, что на производительность это негативно не скажется, если сделать все правильно. То есть ещё раз - у нас будет единый поток, где в зависимости от потребностей фронт-энда будет запуск (текущей итерации) в режиме обычного фаззинга, либо же в режиме cmplog. Сделать это не так сложно, достаточно при записи в канал на стороне фронта (смотри в начале этого поста) добавить некий маркер, который будет прочитан сперва форк-сервером, а после записан уже форк-сервером в потомка, прочитан потомком.... Стоп. Вот и пошли коллизии и накладки дизайна, в данном случае связанность. У нас есть сокеты, и есть пайпы. Раньше у нас был отдельный искусственный поток, который занимался входом-выходом в персистент лупы, передачей кэша предку (за кэш ещё поговорим), и соответственно контролировался сигналом SIGSTOP. Сейчас мы хотим избавиться от сигнала, но и от персистент лупа мы тоже хотим избавиться! Поскольку он не соответствует по определению нашей задаче. Значит кто в таком случае будет передавать кеш? А кто в таком случае будет включать и выключать покрытие? Проблема? Проблема. Давай решать.

Значит, канал при форке сделать придется в любом случае. Как минимум потому, что потомок должен сбрасывать предку переведенные им блоки кода, что бы избежать повторного их перевода после перезапуска. (TCG в QEMU транслирует базовые блоки целевого машинного кода, в машинный код нативной архитектуры, инструментируя их по мере необходимости, это несколько затратно). Так при следующем форке в потомка в памяти уже будут переведенные блоки из предка. Запись в пайп со стороны потомка можно встроить в механизм перевода, он по-моему там и встроен. Таким образом у нас будет нативной кол-во потоков в потомке.

А остальные задачи можно решить используя общую память. Например, мы можем выделить некоторое место под структуру состояния в начале shm страницы, выделенной для передачи ввода в начале каждой итерации, которая будет в себе нести информацию о типе цикла (обычный/цмплог), о размере ввода и вводе, и о потенциальном падении потомка. А так же о флагах глобального, состояния - к примеру сетевой дескриптор установил соединение - значит глобально мы начинаем цикл.

Таким образом у нас будет отдельная спец-страница, в которую будут читать и писать все три участника. Каждый в свой кусочек структуры.

На сегодня все, завтра продолжим.
 
Последнее редактирование:
Список возможных проблем:
1) Узкое место в форк-сервере: сетевая коммуникация + читалка Кеша + отслеживание состояния потомка.
2) Баги в cmplog rtn.
3) Повышенная нагрузка при интеграции cmplog.
4) Возможные проблемы в обработчике исключений.
5) Вариативная, зависящая от архитектуры, структура с информацией о падении.
6) Потенциальная возможность повреждения shm памяти с нашей структурой на стороне целевого приложения (внезапно).
7) Невозможность транслировать предком код, оригинал которого не загружен в память предка.
8) Проблемы сетевой коммуникация под непредвиденной нагрузкой - сокеты в TIME_WAIT, не закрытые дескрипторы, ожидание большего количества ввода со стороны потомка.
9) Риск того, что наш ресурс, его дескриптор, может быть закрыт логикой целевого приложения.
10) Риск выгореть до тла, и не успеть доделать проект.


Описание проблем:
1) Итак ещё разочек.
Есть функция форк-сервера, нить (единственная) заходит в начало while() цикла, и блокируется на чтении из канала к фронту.

В коде стокового qemuafl это выглядит вот так:
C:
/
* Fork server logic, invoked once we hit _start. */
// либо, там, где мы выставили переменной окружения, не обязательно в _start.
// \
     вообще бородатые сеньйоры пожурили бы меня за комменты в два слеша, в файле сорцев, а не в файле хедеров, и убили бы за строчки длиннее 125 чаров, но я для простоты себе это позволю :3

void afl_forkserver(CPUState *cpu) {

  // u32           map_size = 0;
  unsigned char tmp[4] = {0};

  if (forkserver_installed == 1) return;
  forkserver_installed = 1;

  if (getenv("AFL_QEMU_DEBUG_MAPS")) open_self_maps(cpu->env_ptr, 1);

  pid_t child_pid;
  int   t_fd[2];
  u8    child_stopped = 0;
  u32   was_killed;
  int   status = 0;

  // with the max ID value
  if (MAP_SIZE <= FS_OPT_MAX_MAPSIZE)
    status |= (FS_OPT_SET_MAPSIZE(MAP_SIZE) | FS_OPT_MAPSIZE);
  if (lkm_snapshot) status |= FS_OPT_SNAPSHOT;
  if (sharedmem_fuzzing != 0) status |= FS_OPT_SHDMEM_FUZZ;
  if (status) status |= (FS_OPT_ENABLED | FS_OPT_NEWCMPLOG);
  if (getenv("AFL_DEBUG"))
    fprintf(stderr, "Debug: Sending status %08x\n", status);
  memcpy(tmp, &status, 4);

  /* Tell the parent that we're alive. If the parent doesn't want
     to talk, assume that we're not running in forkserver mode. */

  if (write(FORKSRV_FD + 1, tmp, 4) != 4) return;
  afl_forksrv_pid = getpid();
  int first_run = 1;

  if (sharedmem_fuzzing) {

    if (read(FORKSRV_FD, &was_killed, 4) != 4) exit(2);

    if ((was_killed & (0xffffffff & (FS_OPT_ENABLED | FS_OPT_SHDMEM_FUZZ))) ==
        (FS_OPT_ENABLED | FS_OPT_SHDMEM_FUZZ))
      afl_map_shm_fuzz();
    else {

      fprintf(stderr,
              "[AFL] ERROR: afl-fuzz is old and does not support"
              " shmem input");
      exit(1);

    }

  }

  /* All right, let's await orders... */

  while (1) { /* Тут вот начинается каждая итерация  */

    /* Whoops, parent dead? */
    /* А дальше начинается что-то не очень понятное, но щас все разберем */
    if (read(FORKSRV_FD, &was_killed, 4) != 4) exit(2);  // ждем команду на запуск цикла
                                                         // по имени понятно какие логические значения команда может принимать

    /* If we stopped the child in persistent mode, but there was a race
       condition and afl-fuzz already issued SIGKILL, write off the old
       process. */

    // нас предупреждают, что тут возможно состояние гонки, причем между сигналов!
    // имеется ввиду, что потомок уже отправил сам себе SIGSTOP, но пока мы возились AFL решил отправить потомку SIGKILL.
    if (child_stopped && was_killed) { //тут мы обрабатываем этот кейс

      child_stopped = 0; // это должно быть под ифкейсом, как по мне =)
      if (waitpid(child_pid, &status, 0) < 0) exit(8); // "рипаем зомби".

    }

    if (!child_stopped) { /*
    if (child_stopped == 0) {  возможео минимум в двух случая:
      либо мы его обнулили выше, либо это первая итерация.
      для нас это всегда необходимость создавать нового потомка */

      /* Establish a channel with child to grab translation commands. We'll
       read from t_fd[0], child will write to TSL_FD. */

      if (pipe(t_fd) || dup2(t_fd[1], TSL_FD) < 0) exit(3);
      close(t_fd[1]);

      child_pid = fork(); // форкаемся
      if (child_pid < 0) exit(4);

      if (!child_pid) { // предок

        /* Child process. Close descriptors and run free. */

        afl_fork_child = 1;
        close(FORKSRV_FD);
        close(FORKSRV_FD + 1);
        close(t_fd[0]);
        return;

      }

      /* Parent. */

      close(TSL_FD);

    } else {  // если же мы в персисте и чайлд остановлен, то мы его продолжаем

      /* Special handling for persistent mode: if the child is alive but
         currently stopped, simply restart it with SIGCONT. */

      kill(child_pid, SIGCONT);
      child_stopped = 0;

    }

    /* Parent. */
    // Тут нам нужно будет добавить сокеты и сетевой эмулятор...
    if (write(FORKSRV_FD + 1, &child_pid, 4) != 4) exit(5);
    // с обратной стороны канала оживает AFL, получив уведомление о запущеном потомке в виде PID потомка.

    /* Collect translation requests until child dies and closes the pipe. */
    // отголоски из прошлого. Когда потомок умирал после каждой итерации.
    // у нас же (в стоке) из ожидания в чтении канала с кэшэм предка достают уловкой, покажу позже какой.
    afl_wait_tsl(cpu, t_fd[0]);

    /* Get and relay exit status to parent. */
    // WUNTRACED   также возвращаться, если есть остановленный потомок (но не трассируемый через ptrace(2)). Состояние трассируемого остановленного потомка предоставляется даже если этот аргумент не указан.
    if (waitpid(child_pid, &status, is_persistent ? WUNTRACED : 0) < 0) exit(6);

    /* In persistent mode, the child stops itself with SIGSTOP to indicate
       a successful run. In this case, we want to wake it up without forking
       again. */

    // из waitpid() мы вернемся только когда потомок будет мертв (добровольно или по сигналу), либо остановится.
    if (WIFSTOPPED(status))
      child_stopped = 1;
    else if (unlikely(first_run && is_persistent)) {
      fprintf(stderr, "[AFL] ERROR: no persistent iteration executed\n");
      exit(12);  // Persistent is wrong
    }

    first_run = 0;
    // информируем AFL как потомок завершил цикл
    if (write(FORKSRV_FD + 1, &status, 4) != 4) exit(7);

  }

}

Прочитав 4 байта контрольных данных, нить определяет был ли потомок убит по тайм-ауту. Если да - делает не блокирующий waitpid (). Альтернативна - просто делать не блокирующий waitpid () в начале цикла. Сверять факт смерти либо жизни потомка со статусом из канала. Это позволит нам обработать те редкие и каверзные кейсы, в которых потомок подло помрёт где-то между.
Далее, если потомок мертв - форкаемся, настраиваем канал для передачи кеша от потомка к предку, потомок уходит инициализировался, родитель переходит к узкому моменту.


В коде это выглядит вот так.

Для предка:
C:
/* This is the other side of the same channel. Since timeouts are handled by
   afl-fuzz simply killing the child, we can just wait until the pipe breaks. */

static void afl_wait_tsl(CPUState *cpu, int fd) {

  struct afl_tsl t;
  TranslationBlock *tb, *last_tb;

  if (disable_caching) return;
  while (1) {
    u8 invalid_pc = 0;

    /* Broken pipe means it's time to return to the fork server routine. */
    if (read(fd, &t, sizeof(struct afl_tsl)) != sizeof(struct afl_tsl)) break;

    /* Exit command for persistent */
    // вот это важный момент, я нарочно поменял местами функции кеша родителя и потомка, что бы сперва показать этот минус один. \
      это и есть тот трюк, которым родителя отрывают от ожидания кэша.
    if (t.tb.pc == (target_ulong)(-1)) return;

    tb = afl_tb_lookup(cpu, t.tb.pc, t.tb.cs_base, t.tb.flags, t.tb.cf_mask);
    if (!tb) {

      /* The child may request to transate a block of memory that is not
         mapped in the parent (e.g. jitted code or dlopened code).
         This causes a SIGSEV in gen_intermediate_code() and associated
         subroutines. We simply avoid caching of such blocks.
         Более того, зис куд симпли генерейт дифферентли чейнед блокс, ин фьючер,
         мэйкинг АФЛ синк, зэт аур ковераж мап ис анстэйбл, мэйкинг ас сэд =)
         решением в данном случае будет передача родителю системных вызовов mmap.
         он просто (должен будет) мапит те же библиотеки в то же адресное пространство.
          и потом уже транслирует код
       */

      if (is_valid_addr(t.tb.pc)) {

        // вот это ниже мьютекс, который тут не с проста. Пока что просто запомним, что он тут есть.
        mmap_lock();
        tb = tb_gen_code(cpu, t.tb.pc, t.tb.cs_base, t.tb.flags, t.tb.cf_mask);
        mmap_unlock();

      } else {

        invalid_pc = 1;

      }

    }

    if (t.is_chain && !invalid_pc) {

      last_tb = afl_tb_lookup(cpu, t.chain.last_tb.pc,
                                 t.chain.last_tb.cs_base,
                                 t.chain.last_tb.flags,
                                 t.chain.cf_mask);
#define TB_JMP_RESET_OFFSET_INVALID 0xffff
        if (last_tb && (last_tb->jmp_reset_offset[t.chain.tb_exit] !=
                        TB_JMP_RESET_OFFSET_INVALID)) {

          tb_add_jump(last_tb, t.chain.tb_exit, tb);

        }

    }

  }

  close(fd);

}

Для потомка:
C:
/* This code is invoked whenever QEMU decides that it doesn't have a
   translation of a particular block and needs to compute it, or when it
   decides to chain two TBs together. When this happens, we tell the parent to
   mirror the operation, so that the next fork() has a cached copy. */

static void afl_request_tsl(target_ulong pc, target_ulong cb, uint32_t flags,
                            uint32_t cf_mask, TranslationBlock *last_tb,
                            int tb_exit) {

  if (disable_caching) return;

  struct afl_tsl t;

  if (!afl_fork_child) return;

  t.tb.pc = pc;
  t.tb.cs_base = cb;
  t.tb.flags = flags;
  t.tb.cf_mask = cf_mask;
  t.is_chain = (last_tb != NULL);

  if (t.is_chain) {

    t.chain.last_tb.pc = last_tb->pc;
    t.chain.last_tb.cs_base = last_tb->cs_base;
    t.chain.last_tb.flags = last_tb->flags;
    t.chain.cf_mask = cf_mask;
    t.chain.tb_exit = tb_exit;

  }
 
  if (write(TSL_FD, &t, sizeof(struct afl_tsl)) != sizeof(struct afl_tsl))
    return;

}

И наконец, код пишущий этот самый -1 в канал кэша:
C:
/* A simplified persistent mode handler, used as explained in
 * llvm_mode/README.md. */

static u32 cycle_cnt;

void afl_persistent_iter(CPUArchState *env) {

  static struct afl_tsl exit_cmd_tsl;
  // если не исчерпали кол-во итераций до перезапуска
  if (!afl_persistent_cnt || --cycle_cnt) {
 
    // если включена опция снапшотов памяти - восстанавливаем состояние страниц
    if (persistent_memory) restore_memory_snapshot();
     
    // если включен снапшот состояния регистров - восстанавливаем состояние регистров
    // исключение - режим подгрузки либы персистент лупа.
    // почитать про нее можешь и сам, иначе этот дред никогда не закончится
    if (persistent_save_gpr && !afl_persistent_hook_ptr) {
      afl_restore_regs(&saved_regs, env);
    }

    if (!disable_caching) {
 
      memset(&exit_cmd_tsl, 0, sizeof(struct afl_tsl));
      exit_cmd_tsl.tb.pc = (target_ulong)(-1); // маякуем предку что итерация подошла к концу

      if (write(TSL_FD, &exit_cmd_tsl, sizeof(struct afl_tsl)) !=
          sizeof(struct afl_tsl)) {

        /* Exit the persistent loop on pipe error */
        afl_area_ptr = dummy;
        exit(0);

      }
   
    }

    // TODO use only pipe
    // Я с увжением отношусь к немцам написавшим AFL++
    raise(SIGSTOP);

   
    // now we have shared_buf updated and ready to use
    if (persistent_save_gpr && afl_persistent_hook_ptr) {
   
      struct api_regs hook_regs = saved_regs;
      afl_persistent_hook_ptr(&hook_regs, guest_base, shared_buf,
                              *shared_buf_len);
      afl_restore_regs(&hook_regs, env);

    }

    // боль
    afl_area_ptr[0] = 1;
    afl_prev_loc = 0;

  } else {

    // способ выключить покрытие на минималках
    afl_area_ptr = dummy;
    exit(0);

  }

}


Тут суть заключается в том, что нам нужно вычитывать из одного дескриптора кеш, иначе потомок заблокируется при трансляции кода, когда буфер дескриптора заполнится, при этом всем подключиться сокетом к потомку, передать в него ввод, и дождаться либо ответа либо тайм-аута.
Мне кажется, что нужно добавить новый тред в предка. Либо же играться с не блокирующими дескрипторами. Второй вариант усложнит код, и добавит лишних вычислений, хотя вычисления тут не будут накладными, а вот вероятность выстрелить себе в ногу возрастает. Первый вариант, в идеале, будет просто работать.
На практике могут возникнуть каверзные подвохи где-то в районе блокировки в момент осуществления перевода и клонирования нового потомка.
То есть, если нить-обработчик кэша, вычитывает из канала N блоков, поштучно, переводя следом каждый блок, то возможна ситуация, где нить сетевой эмулятор (и по совместительству форксервер) в этот момент вызывает fork().
Тогда пространство памяти процесса будет скопировано в потомка, который родится внутри функции форк-серва, но, например, с заблокированным (нитью-переводчиком в момент перевода) мьютексом.
И дойдя до необходимости перевести блок уйдет в дедлок. Решить такое можно спинлоком =) , либо мьютексом. Семафорами не целесообразно.
Мьютексами можно, т.к. мы не в обработчике сигналов.
В чужих контекстах обычно используют атомарную синхронизацию.
Спинлоки атомарны, и работают без дополнительных системных вызовов, без смены контекста.
Мьютексы уходят во фьютексы, а фьютексы и семафоры - это системный вызов, что само по себе дорого, т.к. это смена контекста в ядро и обратно.
Это был небольшой ликбез по примитивам синхронизации в системном программировании.

В общем, я думаю мы не будем страдать и возьмём обычный мьютекс, который там уже даже есть для особо одаренных любителей понатыкать ненужной синхронизации, вроде меня.
Будем захватывать его всякий раз, на выходе из чтения из канала, перед началом перевода. И перед входом в чтение отпускать. В форк-сервере же обвяжем логику форка, что бы в нее зайти можно было лишь тогда, когда переводчик не переводит.

Далее, предок должен ответить фронту контрольными 4 байтами - номер процесса потомка.
В случае тайм-аута фронт отправляет SIGKILL по этому номеру. Тут ещё один узкий момент.
В стоке понятие тайм-аута было однозначно. В нашем случае - понятие тайм-аута расширено; может наступить сетевой тайм-аут, либо же потомок парсит инпут и занят (чаще редкость), а может зависнуть весь процесс.
С точки зрения предка это будет выглядеть одинаково - нету результата из сокета.
По факту же, целевой софт может где-то сломаться и уйти в инфинити луп - и в этом бесконечном цикле, своими не ответами в сокет, убить карту покрытия. За факт осуществления возврата из connect зацепиться не получится, т.к. ядро обработает наш connect (и последующий send), даже если потомок ещё не вызвал accept.

Вот это да. И что-же делать? Из приходящего мне на ум - это костыль в виде некоторого заведомо отвечающего ввода. Но это костыль, стрёмный костыль. Т.к. придется выключать покрытие и затормаживать ожидающий AFL, и...
И вообще, может я хочу чтоб кто-то из читателей предложил идею :) Ладно, оставим это на потом. Кстати, если это кто-то читает - то ваши идеи приветствуются.

Кроме того, раз речь идёт за тайм-ауты, вспомним про пункт 8. Касательно статусов TIME_WAIT - есть решение в виде SO_REUSEADDR. Правда его придется добавить в гостевую часть потомка.
1711304890766.png

Есть ещё более низкоуровневые способы ускорить процесс, например fastopen, но не будем страдать:)

Знать где находится потомок - внутри вызова связанного с ожиданием входных данных, или нет - не проблема. Если не было входа в такие сисколлы с таким номером дескриптора - то значит нет. Однако, это будет работать для блокирующей семантики.
Для не блокирующей - нам придется опрашивать события (хукать всякие poll, select, recv), и парсить результаты, в конечном итоге мы упремся в epfd и `man epoll_create` в одном из таргетов, там придется следить уже за целой матрицей возможных комбинаций событий и дескрипторов.
И тогда перейдем к правому сладу на картинке выше.
Если у кого есть идеи - не стесняйтесь.
Пока ограничимся таймаутом заданным при запуске, и хуком на accept в guest side, который, по идее должен, поставить меточку в нашу структурку в начале shm.

По поводу утечек дескриптора - ну тут сам придумаешь уже свой цикл `for (;;) { close(); }`
По поводу оберегания наших дескрипторов от таких же циклов, как я привел выше, только со стороны кода целевого приложения (например bash так делает), то это хук на close() на стороне гостя.
По поводу трансляции кода библиотек, которые подгрузились уже после форка - обсудили вроде в комментах. Я надеюсь сработает.
На счет структуры - подгоним ее как union.
Нагрузки из-за интеграции cmplog и баги cmplog_rtn. Такс. Это жирная тема. И очень потужная. На самом деле я бы с удовольствием нашел бы тут человека (взрослого дядю), кто любит ассемблер (особенно RISC-V машин) и понимает как работают компиляторы (особенно с оптимизацией), и помог бы доработать нужные кусочки кода, инструментирующего таргет функционалом cmplog.
Тогда это было бы просто и решительно - кибер-оружие.

Ладно, кодесы. И так кровавая королева в с немецким акцентом)))

C:
void HELPER(afl_cmplog_rtn)(CPUArchState *env) {

#if defined(TARGET_X86_64)

  target_ulong arg1 = env->regs[R_EDI];
  target_ulong arg2 = env->regs[R_ESI];

#elif defined(TARGET_I386)
  // AFL_G2H() конвертирует содержимое гостевога регистра, то бишь гостевой адрес памяти, в доменный.
  target_ulong *stack = AFL_G2H(env->regs[R_ESP]);
 
  if (!access_ok(env_cpu(env), VERIFY_READ, env->regs[R_ESP],
                 sizeof(target_ulong) * 2))
    return;

  // when this hook is executed, the retaddr is not on stack yet
  target_ulong arg1 = stack[0];
  target_ulong arg2 = stack[1];
   // море-овер, вен зыс хук экхэкутед, другие потоки выполняются.
    // и выше тоже. на любой архитектуре это будет не безопасно.

#else

  // stupid code to make it compile
  target_ulong arg1 = 0;
  target_ulong arg2 = 0;
  return;

#endif

  if (!access_ok(env_cpu(env), VERIFY_READ, arg1, 0x20) ||
      !access_ok(env_cpu(env), VERIFY_READ, arg2, 0x20))
    return;

  void *ptr1 = AFL_G2H(arg1);
  void *ptr2 = AFL_G2H(arg2);

#if defined(TARGET_X86_64) || defined(TARGET_I386)
  uintptr_t k = (uintptr_t)env->eip;
#else
  uintptr_t k = 0;
#endif

  k = (uintptr_t)(afl_hash_ip((uint64_t)k));
  k &= (CMP_MAP_W - 1);

  u32 hits = 0;

  if (__afl_cmp_map->headers[k].type != CMP_TYPE_RTN) {
    __afl_cmp_map->headers[k].type = CMP_TYPE_RTN;
    __afl_cmp_map->headers[k].hits = 0;
    __afl_cmp_map->headers[k].shape = 30;
  } else {
    hits = __afl_cmp_map->headers[k].hits;
  }

  __afl_cmp_map->headers[k].hits += 1;

  hits &= CMP_MAP_RTN_H - 1;
  ((struct cmpfn_operands *)__afl_cmp_map->log[k])[hits].v0_len = 31;
  ((struct cmpfn_operands *)__afl_cmp_map->log[k])[hits].v1_len = 31;
   
    // перфоманс для отцов =)
  __builtin_memcpy(((struct cmpfn_operands *)__afl_cmp_map->log[k])[hits].v0,
                   ptr1, 31);
  __builtin_memcpy(((struct cmpfn_operands *)__afl_cmp_map->log[k])[hits].v1,
                   ptr2, 31);
    // тихие краши в cmplog вместо прохождения роад-блоков для деток
    // мидлы юзают аналог copy_from_user() по ссылке ниже

}

То как я это переписывал, на самом деле стремно показывать... -> тут <- половину надо выкинуть. Если 2/3. Но, мне не у кого особо было спросить. Доминик Маер больше по RUST, чем по системному, а Андреа - не разговорчивый суровый сеньёр.
Но это все детали, мы с вами напишем как надо =)

Из того что показать точно стоит:
C:
if (likely(!qatomic_mb_read(&cmplog_mode)) || !AFLGETCOV())
    return;

Конструкция likely, она же __builtin_expect - делает так, что конвейер процессора на упреждение выполняет вероятное условие. Кароче говоря ощутимых затрат процессорного времени там не будет.
cmplog_mode флажечек, атомарен. выход в ситуации с выключенным покрытием !AFLGETCOV() с лихвой компенсирует затраты на бранч.

C:
extern __thread bool aflThreadCov; // per_thread переменная. Аналог ядерной __per_cpu.

static inline void aflSetCovVal(bool x);
static inline bool aflAskCovVal(void);
static inline bool aflGetCovVal(void);
static inline void aflSwitchThrCov(bool cov);

// return true if global and thread coverage enabled
#define AFLGETCOV()     aflGetCovVal()
// return true if global coverage is enabled
#define AFLASKCOV()     aflAskCovVal()
// set @x to global coverage val
#define AFLSETCOV(x)    aflSetCovVal((bool)(x))
// set current thread coverage flag
#define AFLSWITCH(x)    aflSwitchThrCov(x)
// check @x (cmd from AFL) and return true if it is cmplog cycle
#define AFLGETCMP(x)    ((((x) >> (sizeof(char) * 3 * 8)) & 0xFF) == 0xCC)

static inline void aflSetCovVal(bool x) {
    qatomic_set(&aflTargetBusy, x);
}

// глобально (между Pre-Fuzz и Post-Fuzz)
static inline bool aflAskCovVal(void) {
    return qatomic_mb_read(&aflTargetBusy);
}

static inline bool aflGetCovVal(void) {
    // каст к булевому            в треде       либо включенна опция во всех тредах
    return !!(aflAskCovVal() & (aflThreadCov | afl_all_thr_in_cov));
}

// атомарна т.к. доступается к ней только владелец.
static inline void aflSwitchThrCov(bool cov) { aflThreadCov = cov; }

Ну и потом, в хелпере:
C:
void HELPER(afl_maybe_log)(target_ulong curr, target_ulong next)
{

    if (AFLGETCOV()) {

        register uintptr_t afl_idx = aflHashIp( next, curr, afl_inst_rms );
        // тут все атомарно, через интринсики (intrinsics)
        INC_AFL_AREA(afl_idx);
        // Интринсики завернуты во враперы qatomic_*()
        // Они мне так понравились, что я их выпилил в отдельный заголовок и тягаю с собой в другие проекты
      // aflPrevLoc - это адрес предыдущего блока. почему он хранится читай выше постом.
    } aflPrevLoc = zeroCut(curr) >> 1;
}

/* Generates TCG code for AFL's tracing instrumentation. */
void afl_gen_trace(target_ulong curr, target_ulong next, bool isJmp) {

    switch (afl_insr_policy) { // дуристика ...
        case 1:
            if ( isJmp || !(afl_must_instrument(curr) && afl_must_instrument(next)) )
                return;
        break;
        default:
        case 2:
            if ( !afl_must_instrument(curr) )
                return;
        break;
        case 3:
            if ( !(afl_must_instrument(curr) || afl_must_instrument(next)) )
                return;
        break;
    }

    TCGv curr_v = tcg_const_tl(curr);
    TCGv next_v = tcg_const_tl(next);
    tcg_gen_mb(TCG_MO_ALL | TCG_BAR_SC); // даже бл*ть в TCG нашел примитивы синхронищации, это п*здец
    gen_helper_afl_maybe_log(curr_v, next_v);
    tcg_gen_mb(TCG_MO_ALL | TCG_BAR_SC);
    tcg_temp_free(next_v);
    tcg_temp_free(curr_v);
}

// движек генератора JIT кода. Промежуточное представление между машинным кодом гостя и домена.

/* Called with mmap_lock held for user mode emulation.  */
TranslationBlock *tb_gen_code(CPUState *cpu,
                              target_ulong pc, target_ulong cs_base,
                              uint32_t flags, int cflags)
{
    CPUArchState *env = cpu->env_ptr;
    TranslationBlock *tb, *existing_tb;
    tb_page_addr_t phys_pc;
    tcg_insn_unit *gen_code_buf;
    int gen_code_size, search_size, max_insns;
#ifdef CONFIG_PROFILER
    TCGProfile *prof = &tcg_ctx->prof;
    int64_t ti;
#endif
    void *host_pc;

    assert_memory_lock();
    qemu_thread_jit_write();
   
    ... snipped ...
               
    tcg_func_start(tcg_ctx);

    tcg_ctx->cpu = env_cpu(env); // указатель на местную task_struct используется для получения указателя на __cpu

    curr_tb->good = 1; // вместо cur_block_is_good. Предварительно был добавлен в структуру  struct TranslationBlock, выдержка ниже
    tb->exec_cnt = 0; // так же. используется для трассировщика
    tb->red = AFLASKCOV(); // для трассировщика. Смысл: если блок переводится когда тред + цикл в "рабочем окне" - блок красный (горячий) и на него хорошо ставить пре/пост точки.
    tb->determinated = aflThreadId; // для трассировщика. Смысл: если блок выполняется одним и тем же TID - детерменирован.
   
    // хелпер трассера
    afl_gen_btrace(tb);
   
                          // мудак
    afl_gen_trace(tb->pc, 0x12345, 0);

    gen_intermediate_code(cpu, tb, max_insns, pc, host_pc);

    afl_gen_fncov(pc);

    assert(tb->size != 0);
    tcg_ctx->cpu = NULL;
    max_insns = tb->icount;

    // родной трассировщик...)
    trace_translate_block(tb, tb->pc, tb->tc.ptr);

    ...snipped...
}

Структура:
C:
struct TranslationBlock {
    target_ulong pc;   /* simulated PC corresponding to this block (EIP + CS base) */
    target_ulong cs_base; /* CS base for this block */
    uint32_t flags; /* flags defining in which context the code was generated */
    uint32_t cflags;    /* compile flags */

... snipped ...

    /*
     * Each TB has a NULL-terminated list (jmp_list_head) of incoming jumps.
     * Each TB can have two outgoing jumps, and therefore can participate
     * in two lists. The list entries are kept in jmp_list_next[2]. The least
     * significant bit (LSB) of the pointers in these lists is used to encode
     * which of the two list entries is to be used in the pointed TB.
     *
     * List traversals are protected by jmp_lock. The destination TB of each
     * outgoing jump is kept in jmp_dest[] so that the appropriate jmp_lock
     * can be acquired from any origin TB.
     *
     * jmp_dest[] are tagged pointers as well. The LSB is set when the TB is
     * being invalidated, so that no further outgoing jumps from it can be set.
     *
     * jmp_lock also protects the CF_INVALID cflag; a jump must not be chained
     * to a destination TB that has CF_INVALID set.
     */
    uintptr_t jmp_list_head;
    uintptr_t jmp_list_next[2];
    uintptr_t jmp_dest[2];

    uint64_t exec_cnt;
    uint8_t  red;
    uint8_t  determinated;
    uint8_t  good;              /* will be true if aflMustInstrument(tb->pc) return true  */
};

Трассер:
C:
static void afl_gen_btrace(void *tb_ptr) {
    // трассер включен? Если нет все что ниже не выполняется
    if (likely(!afl_trace_exec_blocks))
        return;

    // что такое TCGv_ptr, почему оно используется - в инете есть. С удовольствием послушаю в твоем исполнении)
    TCGv_ptr tb_v = tcg_const_ptr(tb_ptr);

    gen_helper_afl_maybe_trace(tb_v); // если да в начале каждого блока будет сгенерирован вот тот кодес, что ниже

    tcg_temp_free_ptr(tb_v);

}

void HELPER(afl_maybe_trace)(void * ptr)
{
        afl_trace_executed((TranslationBlock *)ptr);
}
 

static inline void afl_trace_executed(TranslationBlock *current_tb) {

        qatomic_inc(&current_tb->exec_cnt);
   
        if (current_tb->red) // ну такое
            qatomic_set(&current_tb->red, AFLASKCOV());

        // начитавшить Линуса Торвальда
        u8 determ = qatomic_cmpxchg(&current_tb->determinated, (u8)aflThreadId, (u8)aflThreadId);
        if (determ && determ != aflThreadId) {
                qatomic_mb_set(&current_tb->determinated, 0);
                QDEBUGF(cYEL "[INFO]: marking new tb as non-deterministic one  [ %#.16lx ] \n", (long)current_tb->pc);
        }

   
        // afl_tb_exe_cnt_lim и afl_trace_current_num: вот это отличная опция. Как работает покажу ниже
        if (( current_tb->good || afl_trace_cov_full )
                && ( afl_tb_exe_cnt_lim ? (ulong)afl_tb_exe_cnt_lim > current_tb->exec_cnt : !!1 )
                && ( afl_trace_cov_only ? AFLASKCOV() : 1 )
                && ( afl_trace_current_num == qatomic_mb_read(&current_tb->exec_cnt) )) {

                QDEBUGF("[ EXE ] [ %#.16lx ] { %sDET"cRST"|%sRED"cRST" } (B=%ld, T=%ld, I=%ld C=%.8lu)\n",
                    (long)current_tb->pc, current_tb->determinated ? cGRN : cRED, (bool)current_tb->red ? cRED : cGRA,
                        (long)aflTargetBusy, (long)aflThreadCov, (long)aflThreadId,
                                (ulong)current_tb->exec_cnt);
        }
}

1711310600727.png


1711310676150.png

1711310748110.png


Как видишь, я запустил сетевое приложение на старой версии фаззера в режиме трассировки.
Запустил указав 10 итераций.
Трассер на каждой итерации фильтровал для печати лишь те ББ, которые были выполнены ровно столько раз, сколько и циклов. То есть, выполняются ровно раз в цикл. То есть имеют непосредственное отношение к чему? Правильно, к коду интереса.

Я надеюсь, ты не устал, Вам было интересно.
Продолжим позже =)
 
Последнее редактирование:
Пожалуйста, обратите внимание, что пользователь заблокирован
Продолжим позже =)
грусно видеть. столько труда и ноль эмоций. а татушки\шлюшки Лохбитов собирают аншлаг.
тс твой текст - сама подача даже - к-а-е-ф) Удачи тебе! Кротости и терпения.
 
Пожалуйста, обратите внимание, что пользователь заблокирован
грусно видеть. столько труда и ноль эмоций. а татушки\шлюшки Лохбитов собирают аншлаг.
тс твой текст - сама подача даже - к-а-е-ф) Удачи тебе! Кротости и терпения.
вероятно на лохбита лохи велись, а тут такой крутой проект что мало кто поймет вообще для чего он нужен
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх