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

Меня зовут Арина Волошина, я AppSec-инженер в YADRO и занимаюсь тестированием безопасности телеком-продуктов: базовой станции, контроллера базовых станций и системой управления элементами сети. Мы внедрили много разных видов тестирования в эти продукты, но этого оказалось недостаточно. В своих научных исследованиях я занималась генетическими алгоритмами, поэтому решила применить академические знания на практике и реализовать генетику в фаззинге. Что из этого вышло — читайте под катом.

YADRO разрабатывает ряд телеком-продуктов:

  • базовую станцию 2G/4G,

  • опорную сеть 5G,

  • систему управления элементами сети (EMS/NMS).

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

Как и зачем мы улучшаем тестирование

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

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

Мы поставили перед собой такие задачи:

  • создать умные и обучаемые тесты безопасности,

  • научиться лучше определять критические уязвимости, 

  • оптимизировать тесты, если они не находят новых ошибок, но они есть.

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

В качестве примера из телекома возьмем 2G-сеть.

Упрощенная схема второго поколения сотовой связи
Упрощенная схема второго поколения сотовой связи

Давайте кратко разберемся, как устроена даже самая простая сеть 2G, которая существует уже много десятков лет. В ней работают такие устройства, как базовая станция и контроллер базовой станции для 2G/3G. Также есть ядро сети, где располагаются центр коммутации и центр аутентификации. Там хранятся данные абонентов: информация о тарифах, параметрах обслуживания и так далее. Это операторская часть сети.

Теперь перейдем к интерфейсу, который в ранних поколениях связи защищен слабее, чем в более поздних. При этом нам все еще нужно поддерживать и 2G-сети. На практике мы строим решения формата 4G/2G, чтобы сохранять высокое покрытие и совместимость. 

Отказаться от второго поколения мы пока не можем. Именно в таких гибридных сценариях интерфейс между устройствами сети, базовой станцией и контроллером считается наиболее уязвимым и наименее доверенным среди всех связей между элементами сети. К ним можно подключиться, если оставить безопасность в состоянии по умолчанию или не настроить ее вовсе. В таком режиме возможны атаки уровня man-in-the-middle: злоумышленник способен подключиться, перехватить трафик и отправить вредоносные данные.

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

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

Фаззинг: начало 

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

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

Если вам интересно заниматься тестированием и безопасностью в телекоме, то ждем в команде:

QA Automation Engineer Python (LTE)
SDET в 5G Core (Python)
Test Engineer (Performance Test)
Application Security Engineer
DevSecOps

Конфигурация простая: обычная схема клиент-сервер. Фаззер и все инструменты, которые мы к нему подключаем, выступают в роли клиента, а контроллер — сервера: 

Потом мы считываем обратную связь в таком режиме и анализируем поведение системы. 

Особенности реализации

  • Для реализации фаззинга мы выбрали язык C как наиболее удобный, потому что большинство инструментов фаззинга написано как раз под него. 

  • AFLNet используется в качестве основы для поддержания сессии общения между устройствами и отправки сообщений.

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

  • Проект собирается под компилятор afl-gcc, что позволяет считать тестовое покрытие. 

  • Наконец, нужна аутентификация, чтобы контроллер не отбросил нас на первом же шаге, а воспринял как реальную базовую станцию. Это позволяет полностью воспроизвести корректный сценарий установления связи.

Генетический алгоритм

Чтобы увеличить точность обнаружения уязвимостей, которые не удается найти фаззингом, можно использовать генетический алгоритм. Кратко разберем его основные этапы:

Оговорюсь, что на данный момент фаззинг с генетическим алгоритмом в YADRO официально не применяется, пока мы проводим эксперименты.  

Инициализация первичной популяции — это первый этап. На этом шаге мы получаем тестовые инпуты: считываем их из заранее подготовленного набора или генерируем автоматически.

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

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

Этапы кроссовера и мутации достаточно просты, но их можно настраивать по-разному. Кроссовер — это операция создания потомка путем скрещивания двух или более кандидатов. Мутация — операции над битами и байтами данных, в простейшем случае — инверсии, сложения, перестановки и так далее.

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

Мы сами решаем, когда выходить из цикла: по времени, по достижению ожидаемого сбоя или другим критериям. 

Есть много ML-алгоритмов, но именно генетический не требует постоянного переобучения, и у него нет четкой цели. Это полезно в ситуации, когда критерии сложно формализовать и строго определить: есть только набор негативных сценариев, которые заранее полностью предугадать невозможно. Чтобы не переобучать модель постоянно, что сложно и трудоемко, мы выбираем более гибкий подход. Но в некоторых задачах такой метод может быть менее эффективным.

Пример №0

Посмотрим, как можно ускорить фаззинг. Если встроить в него простейшую обратную связь, например, возвращать −1, когда присланные данные не распознаются и не декодируются, то фаззер просто не рассматривает такие варианты дальше. Это базовая схема подсчета отклонений и определения обратной связи:

В сборке приложения появляется небольшой, но заметный прирост скорости фаззинга:

Режим сборки

Обратная связь

Скорость фаззинга (exec/s)

Debug

+

4038 (baseline)

Debug

-

4377 (+8.40%)

DebugOptimized

+

4130 (+2.28%)

DebugOptimized

-

4565 (+13.05%)

В разных режимах сбо��ки этот прирост сохраняется, хотя сама обратная связь остается максимально простой.

Прикручиваем генетику: фаззинг 2.0

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

Фаззер работает больше с состояниями и деревьями и постоянно генерирует новые ветки и исследует разные пути решений. В генетическом алгоритме устанавливается зависимость между поколениями: он учитывает связь между предыдущим и следующим поколением. Поэтому генетика дольше движется в рамках одной ветки и только после того, как долго не находит ничего нового, переходит к следующей. Это делает ее более «неветвистой». 

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

Фаззинг — это, по сути, тестирование «в лоб»: мы подаем на вход большой объем данных и смотрим, как система отреагирует. Генетический же алгоритм больше ориентирован на обучение и интерпретацию: происходит семантический анализ входных данных.

Основные отличия AFLNet от фаззера с генетическим алгоритмом:

AFLNet

Фаззер с генетическим алгоритмом

Учет аварий и покрытия кода приложения

Учет своих симптомов

Бинарное дерево решений

Оценка и веса

Случайные мутации

Мутации + кроссовер и селекция

Тестирование «в лоб»

Интерпретация и обучение

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

Фитнес-функция

Рассмотрим простую фитнес-функцию:

def fitness(response):
      score = 0
      critical_errors = [
      "segfault", "crash", "panic", "null pointer"
      ]
      if any(error in response.lower() for error in
critical_errors):
            score += 10
       if "stack trace" in response.lower():
            score += 3
       if response_time > threshold:
            score += 1
       return score

Мы можем встроить в нее набор ключевых слов. Если эти слова появляются в ответах или логах, то сразу присваиваем максимальную оценку — то есть считаем, что произошло максимально критическое отклонение. Менее значимые метрики получают меньший скоринг. Можно «минусовать» кандидатов, оценивать их по своим правилам — возможна реализация любой логики оценки.

Кроссовер и мутация 

Берем двух родителей и для простоты приводим их к одной длине. Затем в случайном месте разрезаем оба гена и собираем потомка той же длины:

def crossover(parent1, parent2):
      if len(parent1) != len(parent2):
          min_len = min(len(parent1), len(parent2))
          parent1 = parent1[:min_len]
          parent2 = parent2[:min_len]
       point = random.randint(1, len(parent1) - 1)
       child = parent1[:point] + parent2[point:]
       return child

При желании можно брать больше родителей или комбинировать их по-другому, сопоставляя гены так, как вам нужно.

Мутации тоже бывают различных видов. У меня в примере наиболее равномерный вариант. Мы задаем mutation rate, то есть вероятность мутации:

def mutate(data, mutation_rate=0.01):
      mutated = bytearray(data)
      for i in range(len(mutated)):
           if random.random() < mutation_rate:
               mutated[i] = random.randint(0, 255)
      return mutated

В этом примере мы задаем вероятность мутации каждого байта, которому присваивается случайное значение. Все можно адаптировать под свою задачу: если вы работаете не с бинарными протоколами, а, допустим, с REST, многие детали можно опустить, упростить модель и перейти на более низкий протокольный уровень — уже там пробовать вносить мутации.

Собираем алгоритм

Сначала выполняется простейшая инициализация популяции. Я опустила детали того, как именно она формируется: в примере это просто чтение входных данных из очереди. 

def main():
    population = [ ] # Инициализация
    scored_population += [(fitness(path), indiv) for i, indiv in
enumerate(population)] # Оценка
    scored_population.sort(key=lambda x: x[0], reverse=True)
    selected = [ind in scored_population[:len//2]] # Селекция
    new_population = selected.copy() # Репродукция
    while len(new_population) < POP_SIZE:
        p1, p2 = random.sample(selection, 2) # 2 случайных родителя
        child = crossover(p1, p2)
        child = mutate(child, mutation_rate=0.02)
        new_population.append(child)
     for idx, indiv in enumerate(new_population): # Сохраняем
        save_test_case(indiv, QUEUE_DIR, "id", idx)

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

Пример вредоносного пакета 

Посмотрим, что вредоносного может быть в таких простых примерах:

Header (12 байтов):
0x80 0x60 0x00 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

Payload (пример 64 байтов, содержимое в hex):
0x00 0x00 0x00 0x00 // Делитель = 0
0xFF 0xFF 0xFF 0xFF // Для переполнения
0x01 0x00 0x00 0x00 // Значение, вызывающее сбой
0xDE 0xAD 0xBE 0xEF // Специальные флаги и сигнатуры

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

Так можно генерировать переполнения, сбои, встраивать системные вызовы, вредоносные команды или специальные флаги, подмешивая их в полезную нагрузку пакета. Это позволяет целенаправленно искать уязвимости и ошибки.

Затем ожидаем ответ: арифметическая ошибка при обработке полезной нагрузки RTP, попытка деления на ноль или переполнение буфера. В идеале нужно добиться неопределенного поведения системы. В числе ошибок может быть unhandled exception, то есть необработанное исключение, на которое система раньше не умела реагировать.

Примеры общих уязвимостей:

CWE-369

Divide By Zero

CWE-119

Improper Restriction of Operations within the Bounds

of Memory Buffer

CWE-835

Loop with Unreachable Exit Condition ('Infinite Loop')

Как мы развивали генетический алгоритм

Основное направление развития — это улучшение фитнес-функций, мутаций и селекции. Например, в фитнес-функции можно учитывать не только сигнатуры, но и поведение системы в целом. Чем больше факторов включено в оценку, тем точнее становится скоринг и эффективнее сам алгоритм:

def fitness(response):
      behavior_score = evaluate(metrics,log_warnings,vulns)
      #error_rate,response_time,cpu_usage,memory_usage
      score += behavior_score
      bad_patterns = [
      r"invalid (base64|json|xml|protobuf) data",
      r"serialization/deserialization error",
      r"unable to interpret data" #...
      ]
      matches = re.findall(bad_patterns, logs)
      return score

Некоторые виды метрик, которые можно учитывать:

      behavior_score = evaluate(metrics,log_warnings,vulns)
      #error_rate,response_time,cpu_usage,memory_usage

Далее идут более специфические паттерны, которые актуальны для тестирования безопасности в телекоме:

      bad_patterns = [
      r"invalid (base64|json|xml|protobuf) data"
      r"serialization/deserialization error",
      r"unable to interpret data" #...

Это проблемы с обработкой данных, ошибки парсинга, декодирования, а также ошибки интерпретации. Эти паттерны тоже можно интегрировать в оценку. 

Два простых примера улучшения мутации:

def mutate(data, mutation_rate=0.01):
      ...
       mutated_length = random.randint(size + 10, size + 50)
       
       data.insert(insert_pos, random.randint(0x00, 0xFF))
      ...
       return mutated

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

Посмотрим, как «подружить» фитнес-функцию и мутации. На этапе фитнеса мы можем не только расширять пакет, но и просто изменить длину или контрольную сумму, не затрагивая остальные данные:

def fitness()

Magic Header: 0xABCD
Command: 0x10
Length: 0x0008
Payload: 0xDE 0xAD
0xBE 0xEF
Checksum: 0x75

В результате это может вызвать предупреждения или исключения — такие ошибки достаточно распространены:

Parsing error: expected 8 bytes
for payload, but received only 4
bytes.
Error: NullPointerException at
parse_payload()

Exception: unhandled
std::out_of_range in function
parse_payload at offset 0x200

На этапе ��утации, когда фитнес-функция распознает вектор, туда можно попробовать встроить вредоносную последовательность, которая генерируется специально:

def mutate()

Magic Header: 0xABCD
Command: 0x10
Length: 0x0016
Payload: 0xDE 0xAD
0xBE 0xEF 0x00 0x00
0x00 0x00 0x0I 0xAM
0xHE 0xAR 0xTO 0xRU
0xIN 0xYO 0xU0 0x0!
Checksum: 0x89

Получаем уже более серьезную ошибку, а именно — обрыв канала связи:

*** Error in
`/usr/local/bin/myapp`:
free(): invalid pointer:
0x556f3a2d2b10 ***
*** Signal 6 (SIGABRT),
Code: 1 (SI_TKILL) ***
at:
malloc_consolidate.c: 519
Address 0x0 is not
heap-allocated
Address 0x556f3a2d2b10
was (probably) not allocated
Aborted (core dumped)

Фактически мы тестировали взаимодействие между абонентами. Если в таком контексте происходит обрыв, то это приводит к потере данных сигнализации:

E1L(0) Signalling link down
(bts=0) Dropping OML link: link down
trx=0) Lost E1 OML link

Важно, что именно входная последовательность вызвала сбой, а не ошибка настройки.

Сравнение

Посмотрим на вывод AFL-фаззера и ключевые метрики:

Это базовый пример фаззинга без модификаций. За три часа он исследовал большое количество путей (total paths). Видно, что фаззинг идет нарастающим темпом и не зацикливается — он продолжает генерировать новые ветки решений. Вылетов (unique crashes) при этом нет. Также видно, что много путей выбраны как предпочтительные (favored paths), но по ним фаззеру пока не удалось продвинуться дальше, несмотря на длительную работу.

Посмотрим на второй скриншот, на котором приведен пример работы фаззера с генетическим алгоритмом:

Уже есть один вылет (unique crash), и это именно вылет, а не просто долгая задержка, так как на счетчике uniq hang ноль. То есть приложение вылетело именно от посланных нами данных. 

Путей гораздо меньше, всего 30, хотя алгоритм работал только в два раза меньше по сравнению с предыдущим примером. Предпочтительный путь всего один, на котором и подключился генетический алгоритм и начался процесс активной генерации входных данных. То есть была только одна ветка решений. 

Сложности с инфраструктурой и обучаемостью

Все проблемы, которые мы выявили, можно разделить на две части. Первая связана с инфраструктурой — думаю, многие, кто занимался тестированием или фаззингом, с этим сталкивались. Вторая часть касается непосредственно генетического алгоритма. Эти проблемы обычно не пересекаются. 

Инфраструктура

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

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

Также можно использовать loops, в AFL это persistent mode. В этом режиме не нужно каждый раз поднимать всю систему заново: тестирование запускается на выбранном участке, что значительно ускоряет процесс.

Есть и вынужденные решение. В нашем случае это были метод черного ящика (Black Box), прокси и эмуляторы.

Переход к black box-тестированию не самый лучший вариант — он сильно ограничивает обучаемость теста, так как мы можем получать только внешние ответы, логи и прочую информацию, которую система выдает наружу. Хотя логи могут быть доступны не всегда.

Вынос взаимодействия в прокси — не создаем супер-клиент, а оставляем взаимодействие как есть. Фаззинг с генетикой перехватывают трафик, мутируют его и отправляют системе.

Эмуляторы могут быть как простым, так и сложным решением. Все зависит от того, насколько вам важно приблизить тест к реальному продуктовому сценарию.

Обучаемость

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

Можно также симулировать определенные состояния системы. Например, заранее переводить ее в состояние, близкое к непредсказуемому поведению, и уже на этом фоне запускать различные мутации, пытаясь вызвать отказ.

Иногда приходится прибегать к вынужденным решениям: собирать как можно больше данных для обучения из всех доступных источников, чтобы алгоритм имел достаточно контекста для анализа. Речь о логах, дебаге, трассировках и анализе сетевого трафика. 

Пространство для улучшений

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

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

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

Наконец, умная работа с параллелизацией: нужно исключить ситуации, когда один процесс простаивает, пока другой работает, и добиться согласованной работы фаззера и генетики. Фаззеру важно понимать момент, когда стоит подключить генетический алгоритм, например, когда конкретная ветка начинает вызывать сбои или заметные задержки, и на ней уже есть смысл применять более интеллектуальные мутации. Со своей стороны генетическому алгоритму нужно уметь ориентироваться в процессе фаззинга и интерпретировать массив сгенерированных им мусорных данных.

Улучшение помогут находить скрытые и трудно воспроизводимые ошибки, такие как: 

  • Глубинные буферные переполнения — это переполнение не просто при приеме пакетов, а более глубокие, внутренние. Это один из векторов для улучшения тестирования.

  • Состояния гонок — фаззеры обычно нацелены на их поиск, но редко когда удается достичь успешного выявления.

  • Аномальное поведение — проявляется не только в статике, например, работе с сетевым буфером, но и в динамике системы.

  • Критические сбои в парсерах — ошибки, приводящие к серьезным сбоям при разборе данных.

  • Коррупция структур данных или глобальных состояний системы — вызывает сбои или уязвимости в безопасности.

Генетический алгоритм спешит на помощь

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

Что еще почитать про фаззинг и тестирование в телекоме: